55
66package org.jetbrains.letsPlot.compose
77
8- import androidx.compose.foundation.Image
9- import androidx.compose.foundation.clickable
8+ import androidx.compose.foundation.*
9+ import androidx.compose.foundation.interaction.MutableInteractionSource
10+ import androidx.compose.foundation.interaction.collectIsHoveredAsState
1011import androidx.compose.foundation.layout.*
12+ import androidx.compose.foundation.shape.RoundedCornerShape
1113import androidx.compose.runtime.*
14+ import androidx.compose.ui.Alignment
1215import androidx.compose.ui.Modifier
13- import androidx.compose.ui.graphics.ImageBitmap
14- import androidx.compose.ui.graphics.toComposeImageBitmap
16+ import androidx.compose.ui.draw.clip
17+ import androidx.compose.ui.graphics.Color
1518import androidx.compose.ui.unit.dp
1619import org.jetbrains.letsPlot.commons.registration.Registration
1720import org.jetbrains.letsPlot.core.plot.builder.interact.tools.DefaultFigureToolsController
@@ -20,10 +23,7 @@ import org.jetbrains.letsPlot.core.plot.builder.interact.tools.ToggleToolModel
2023import org.jetbrains.letsPlot.core.plot.builder.interact.tools.ToolSpecs.BBOX_ZOOM_TOOL_SPEC
2124import org.jetbrains.letsPlot.core.plot.builder.interact.tools.ToolSpecs.CBOX_ZOOM_TOOL_SPEC
2225import org.jetbrains.letsPlot.core.plot.builder.interact.tools.ToolSpecs.PAN_TOOL_SPEC
23- import java.awt.image.BufferedImage
24- import java.io.ByteArrayInputStream
25- import java.util.*
26- import javax.imageio.ImageIO
26+ import org.jetbrains.letsPlot.core.plot.builder.interact.tools.res.ToolbarIcons
2727
2828
2929@Suppress(" FunctionName" )
@@ -76,75 +76,121 @@ fun PlotToolbar(figureModel: PlotFigureModel) {
7676 }
7777 }
7878
79+ // Toolbar container - matches PlotPanelToolbar.kt (Swing)
80+ //
81+ // Expected height: 33px
82+ // (org.jetbrains.letsPlot.core.plot.builder.presentation.Defaults.TOOLBAR_HEIGHT)
7983 Row (
80- modifier = Modifier .fillMaxWidth(),
81- horizontalArrangement = Arrangement .Center
84+ modifier = Modifier
85+ .fillMaxWidth()
86+ .height(33 .dp),
87+ horizontalArrangement = Arrangement .Center ,
88+ verticalAlignment = Alignment .CenterVertically
8289 ) {
83- IconButton (
84- icon = if (panToolState) PAN_ACTIVE_ICON else PAN_ICON ,
85- modifier = Modifier .padding(3 .dp).size(24 .dp),
86- onClick = { panToolModel.action() }
87- )
88-
89- IconButton (
90- icon = if (bboxZoomToolState) BBOX_ZOOM_ACTIVE_ICON else BBOX_ZOOM_ICON ,
91- modifier = Modifier .padding(3 .dp).size(24 .dp),
92- onClick = { bboxZoomToolModel.action() }
93- )
94-
95- IconButton (
96- icon = if (cboxZoomToolState) CBOX_ZOOM_ACTIVE_ICON else CBOX_ZOOM_ICON ,
97- modifier = Modifier .padding(3 .dp).size(24 .dp),
98- onClick = { cboxZoomToolModel.action() }
99- )
100-
101- IconButton (
102- icon = RESET_ICON ,
103- modifier = Modifier .padding(3 .dp).size(24 .dp),
104- onClick = { controller.resetFigure(deactiveTools = true ) }
105- )
90+ Row (
91+ modifier = Modifier
92+ .clip(RoundedCornerShape (8 .dp))
93+ .background(C_BACKGR_TRANSPARENT )
94+ .border(
95+ BorderStroke (1 .dp, Color (200 , 200 , 200 )),
96+ RoundedCornerShape (8 .dp)
97+ )
98+ .padding(horizontal = 5 .dp, vertical = 2 .dp),
99+ horizontalArrangement = Arrangement .spacedBy(6 .dp)
100+ ) {
101+ SvgIconButton (
102+ svgString = ToolbarIcons .PAN_TOOL ,
103+ isSelected = panToolState,
104+ onClick = { panToolModel.action() },
105+ contentDescription = " Pan"
106+ )
107+
108+ SvgIconButton (
109+ svgString = ToolbarIcons .ZOOM_CORNER ,
110+ isSelected = bboxZoomToolState,
111+ onClick = { bboxZoomToolModel.action() },
112+ contentDescription = " Rubber Band Zoom"
113+ )
114+
115+ SvgIconButton (
116+ svgString = ToolbarIcons .ZOOM_CENTER ,
117+ isSelected = cboxZoomToolState,
118+ onClick = { cboxZoomToolModel.action() },
119+ contentDescription = " Centerpoint Zoom"
120+ )
121+
122+ SvgIconButton (
123+ svgString = ToolbarIcons .RESET ,
124+ isSelected = false ,
125+ onClick = { controller.resetFigure(deactiveTools = true ) },
126+ contentDescription = " Reset"
127+ )
128+ }
106129 }
107130}
108131
132+ @Composable
133+ private fun SvgIconButton (
134+ svgString : String ,
135+ isSelected : Boolean ,
136+ onClick : () -> Unit ,
137+ contentDescription : String ,
138+ modifier : Modifier = Modifier
139+ ) {
140+ val interactionSource = remember { MutableInteractionSource () }
141+ val isHovered by interactionSource.collectIsHoveredAsState()
142+
143+ val iconColor = when {
144+ isSelected -> C_STROKE_SEL // White when selected
145+ else -> C_STROKE // Gray for normal and hover
146+ }
109147
110- private fun base64ToImageBitmap (base64 : String ): ImageBitmap {
111- val base64Data = base64.substringAfter(" base64," )
112- val decodedBytes = Base64 .getDecoder().decode(base64Data)
113- val inputStream = ByteArrayInputStream (decodedBytes)
114- val bufferedImage: BufferedImage = ImageIO .read(inputStream)
115- return bufferedImage.toComposeImageBitmap()
116- }
148+ val backgroundColor = when {
149+ isSelected -> C_BACKGR_SEL // Blue when selected
150+ isHovered -> C_BACKGR_HOVER // Light gray when hovering
151+ else -> Color .Transparent // Transparent for a normal state
152+ }
153+
154+ val icon = SvgIconUtils .rememberSvgIcon(
155+ svgString = svgString,
156+ iconColor = iconColor
157+ )
117158
118- @Composable
119- private fun IconButton (icon : ImageBitmap , modifier : Modifier = Modifier , onClick : () -> Unit ) {
120159 Box (
121- modifier = Modifier
122- .clickable(onClick = onClick)
123- .then(modifier)
160+ modifier = modifier
161+ .size(22 .dp)
162+ .clip(RoundedCornerShape (4 .dp))
163+ .background(backgroundColor)
164+ .hoverable(interactionSource)
165+ .clickable(
166+ interactionSource = interactionSource,
167+ indication = null ,
168+ onClick = onClick
169+ ),
170+ contentAlignment = Alignment .Center
124171 ) {
125172 Image (
126- bitmap = icon,
127- contentDescription = null ,
128- modifier = Modifier .size(icon.width.dp, icon.height .dp)
173+ painter = icon,
174+ contentDescription = contentDescription ,
175+ modifier = Modifier .size(16 .dp)
129176 )
130177 }
131178}
132179
133- private val PAN_ICON =
134- base64ToImageBitmap(" data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB2HAAAdhwGP5fFlAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAPNJREFUWIXtlVEOgyAQRGd6h5J6RDxu/ehxph/FhiiILKbUxE32B2XmuS4LJaFn3Lq6twKQFMmmEp67AhfATwBIepKuVpikI+mLL0rKJoARgABMAFziuT4Sq3UX9gjAuOlRALgDeAahF4ChBJDY8zADlCCWALXmuwC2IGIAi/lugBzEDGA1rwJINNcUAcRrq2Y9DCBRiTirvnxOBlG0Xiq1IYnAH0zCo3/BUK13miZE4qghfwx3V8JsHta3BtEuCLP5EsAKYTZPAVggSgB9r+Mg5nOdnQOIIHxJ/zsJLTFPT4WpZonuk/AC6A7Q1IRHRPcKvAGpnEGs1cIJFQAAAABJRU5ErkJggg==" )
135- private val PAN_ACTIVE_ICON =
136- base64ToImageBitmap(" data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB2HAAAdhwGP5fFlAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAi5JREFUWIXNl0FPE0EcxX8zu4lgirQeBDEmeJKLypHQ6IEoeunFLyGNMSbW1n4CUyBNuGj7Gbx4aCKRhIum6lVunGiC2koi25YGMdnd8bCyKUa3O0tKeafNzn/nvZl582ZHKKW4+7Q+hZIFF+YEjNBHKNgD1k3Hya+uTGyK+cy3KaXkRyDeT+J/oGk4zoxEyUJU8rXiGGvFsagC4o6Uz6ULc1F7ODaEuC37veY9cE4OkByA0y8gNTtMPKavMx6TpGaHe9aZgeTJszy6P0IqaZMrWTQ7bmjypXSCyXEThKBS3f9vbeDQ3n0+YKtuMzluUnyY4HwIy4zGJIsLHvn2jk114yCwPrDHVsclV7bYqttcvmCynA4WMRqTLC0kuHLRI8++tNjdC561nkMKKyIKeSgBYUREJQcQd540VKhKjpqr1vC8AfjPtYaeWUEzB5odl2zJ8o15iEPD5ct65NA1A8c4VCJhPvMdOAVJqOUBOGq4bmzv2GRLFrttvSXQmoF4TLKc9shrDdt/X2t4u6PwIKEd26Gr/064fNny27qNGTYxtQT02ue6iaklIGzIRBURWKGbcFFEBLbeujHkGy7zIly8tjouz8qWb8zktaHA+sD/gUp1H5Ti/cYvrYRrdlxyJYub189Q+fAzugCgZwdBIsJ8O/AkHLyAP3e1QaElgfWB0QveinuPv151DOMTJ305FfwwDXtarq5MbBqOMwO8BtonQN1G8Mo07Ok3i5e+/AaZ1ys9OkxDiwAAAABJRU5ErkJggg==" )
137-
138- private val BBOX_ZOOM_ICON =
139- base64ToImageBitmap(" data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB2HAAAdhwGP5fFlAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAANtJREFUWIXtlk0KwjAQRt8n3qJdqisP0Gv1NNobWMSVy/Yi3kHBdutiXJhKLAUX9g/MwBCShrxH0oSRmTFlLCalNwKSEkkHSaWkVNJyTIkEeADmZWZmjJEAeQs+VNbACVi3BYqRBJq8AZEvkHZMugDqeasj4OzWz32BJZC14NuBzjt2jHszpsZCkgGYmb78tD9FmzOPdyAIBIEgEASCQBAIAi5qAEnxUDBv7apLoHTtTlI0EHzvusX7g1cubYAr4xSlqze3o3DMeW1R3+AKOPrwj5pwqpjVLfhPgSdBFZH0qgJ4WwAAAABJRU5ErkJggg==" )
140- private val BBOX_ZOOM_ACTIVE_ICON =
141- base64ToImageBitmap(" data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB2HAAAdhwGP5fFlAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAe1JREFUWIXtl89LFGEYxz/vO7OOKesushEYVATFKhQqgR6iIKxuHcL+gToURCxkB08dgmI9CKt3JQg6VBBCKh2EIqEfkEWHQrt02EzQDXenZmdl33k7LDvrggTBTF7me5p5GObzeXnew/MIrTXnb/1Io2XWgzMC4oQYDTawYCo1Op/rWhbnRlbTWsvXQDJM8A7ZNJQaNNEy230wlrx4qo2OdsnbzxVmFh2UF7pAUkl5z+w+FDs7fr0TQ9aqfUdaOLDPJPe4FLoBQgyJFx/K+nRva+isckXzfmWLqWc23zeUX5fxNhk6HGCPJTh5zGIy00kq0WCa775U6D/a0vRxfr3KlbECWgcnkEpIMsMdDPRYXL0Q5+6DIgByZtFh7k25CX7nfjFQOMBG0WPiiQ3AibTl16XyaLpwl7MFvq1Vg6X7ErXet7eKhkAopH9IJBAJRAKRQCQQCUQCvoDj1iaQVMIIDVb/92+3Me34AktftwDIDMebZrYg4ZlLtZ1naaXi1836w/SszfHDMQZ6LB7e3hu4QD224zE1+8t/94+aX1dcGy/w8qPrtyPIOK7m1SeXGxM/Wd02louhm2ulsPfBv6QogYVdgoPguTSVGgU2dwFeMI3qiJzPdS0bSg0CT4H/sBBSQvDINKq9c2P7838AoiGjAsFi+oIAAAAASUVORK5CYII=" )
142-
143- private val CBOX_ZOOM_ICON =
144- base64ToImageBitmap(" data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB2HAAAdhwGP5fFlAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAARFJREFUWIXtlrFuwjAQhr+/Q14ijMCzpk/Q7p1QxMQIrwNdilSyV9chTmSCJaJTQqiak06RHcf/J/vucjIzprSXSdVnAAAzax1YACVQATawV8AWWF1pdsTPIwh3/QzkKYAyLNjFC4ZyIA97G7BJATTHPrh455QN+G7m1NQBSRZiQo5Q6m1dncmzYAb4mwCSMkmvkk6SjpIKSZmLIEoRq4e90qngtsAUPb+90vECnBIAnx4Abwykmogfz0ZegI+ec/fNeQUZdRwcgxdA5rmCuRTPAE8FUAFIWowlFu19SQEcwvNNUj6S+HsY7tsXUX6ugS8e05Qub/4FUeO4oT6ioYUv1I3vMtZsC9FU9lRZ8D8BfgFquENnSQFiqAAAAABJRU5ErkJggg==" )
145- private val CBOX_ZOOM_ACTIVE_ICON =
146- base64ToImageBitmap(" data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB2HAAAdhwGP5fFlAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAjRJREFUWIXtlz9oE1Ecxz/37sqZpmmKxKVuOjQU/ANG2sHFEP8t0qHqqFvtIAXboZObUkGhGR0iVMFBCi7GVkgHcRAhLeIgtK61CibSJM2/kpfncM2lmqA23CUO/U53X9697/e97+/3eKcppbgw9TWIEjNVCGvgw0UoyAFLhpTTC7P9q9r5yY2gUuId0OemcBNs6lIOC5SY6YA4QJ8U4p6oQrgD4hY0LWLszjzg1xkf6SE0YOIxNUe1imXF8to2sZc5vqRkje41dos/mjqIr1s4KlyDx9Q4c8zkxNEuxh6kSWWqANgGxkd68HUL3n8qE53P2gOcQsAvmBjtZWjQZOyyj7tPMwDYyw0NmACuiAOkMlWi8zlLK2javG2glrkb4nUTVvbeA/X6cifwPWDfQMcNGH8f0uQjXePGJS+RUx4UkEgWmVvMU5GqPQauX/Ry9azXfr8Wtp5j8a09z9VSBOdCnkbudCPnmoFmG11t8fhoyUAiWWzklhu5f0FLNTC3mAcgshNFIlnkyQ7XFgMVqYjFt1oqut/R8XNg38D/Y6BQsro74NddE6vNnS/VTxLbwMrnbQAmRn0E/M5vTMCvM3HFuv+urJVt3m7Dx/Ecx490MTRo8uzOIccN1JArVH9pX3up698lNx+mefOhZMfhJAolxduPJW5Ff7BRv5ajRW5/y7r9P/gHZASw1CFx0HgtDCmngc0OiKcNvTIpFmb7V3Uph4EXQLYN0lk0nht65eSr+4fXfwL+SbOhiKONmwAAAABJRU5ErkJggg==" )
147-
148- private val RESET_ICON =
149- base64ToImageBitmap(" data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB2HAAAdhwGP5fFlAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAj5JREFUWIXN17trVEEUx/HPJPERo4kRjREhELSJ2EtQBEFTJSD4akT/AAtr/Qv8CwSxsBKtBAuxND5ADL4IRgyCxEIxKhGMr8qxuLN6s+xu7m6yGweGs3cev/O9h7nnzIYYo5VsbSvqvRJACCGGEBYNSwihLYTQG0LoWgpAR9GFIYR2jGIMezGINWluFtO4gxsxxsnCBDHGBR0xG/77HHASM6W51H9iDl/LxiPuYn+5dqVeEwC9uJ0TfoYz2IX23J4tGMFFfM6tv4TOhgDQj1fp9wccL/RGrMd5fM9Bb24E4Gmyk9haxHmZzlDaW9LaWC9AxFQt+gIQm1IEIq43AlCx1wnRj09p79GWAyTNE2nvC4SaAM3o6VMuHeiD+bmWpOKYUVxOj2P5uVbWggfJDucHQwpR01sIoRM/MB9j7C6NtzICv5JdnR9cABBCmE/VcEkVrkprw29lBbA8Ah+T7WsCwM7kb6YWwPtkdzQBYHeyU7UAxpMdaQJA6fObWDBaljCGZcnitVy5XYZE1IV52RkYrJoJU0RKFez0MgKcS5rjRWrB4bT4nQbKcAW9AXxLb7+vCEDAzQRxD+uW4LwHz5PWlYpramycThsnsK0B59vxMGk8QXdhgJxAif4LzmJtAccdOCXLKREv0Vd1/SJi3bjq311gTnbxPCa7lvfKUusADuEC3uTWX8OGmj4KhvMA7qtyQanQH2G0iHZd1TCEMIQj2CPLbD1YhVm8TYf2VozxcWHNegCa0f6/P6etbn8Amy2DASz9UqwAAAAASUVORK5CYII=" )
180+ private val C_BACKGR = Color (247 , 248 , 250 )
181+ private val C_STROKE = Color (110 , 110 , 110 )
182+ private val C_BACKGR_HOVER = Color (218 , 219 , 221 )
183+ private val C_BACKGR_SEL = Color (69 , 114 , 232 )
184+ private val C_STROKE_SEL = Color .White
185+
186+ private const val ALPHA = 0.8f
187+
188+ // C_BACKGR with an alpha channel which on a white background looks the same as the solid C_BACKGR
189+ // and slightly darkens any darker background.
190+ private val C_BACKGR_TRANSPARENT = Color (
191+ red = (C_BACKGR .red - 1.0f * (1 - ALPHA )) / ALPHA ,
192+ green = (C_BACKGR .green - 1.0f * (1 - ALPHA )) / ALPHA ,
193+ blue = (C_BACKGR .blue - 1.0f * (1 - ALPHA )) / ALPHA ,
194+ alpha = ALPHA
195+ )
150196
0 commit comments