Source code for simvx.core.ui.toolbar
"""Toolbar — horizontal button strip with toggle/radio group support."""
from __future__ import annotations
import logging
from ..descriptors import Signal
from ..math.types import Vec2
from .containers import HBoxContainer
from .core import ThemeColour
from .widgets import Button, Panel
log = logging.getLogger(__name__)
__all__ = ["Toolbar", "ToolbarButton"]
# ============================================================================
# ToolbarButton — Button with toggle and radio group support
# ============================================================================
[docs]
class ToolbarButton(Button):
"""Button that can toggle on/off and participate in radio groups.
When toggle_mode is True, the button stays pressed after clicking.
When group is set, clicking deselects other ToolbarButtons in the
same group (radio behaviour).
Example:
btn = ToolbarButton("Move", toggle=True, group="tools")
btn.toggled.connect(lambda active: print(f"active={active}"))
"""
def __init__(self, text: str = "Button", toggle_mode: bool = False, group: str = "", on_press=None, **kwargs):
super().__init__(text=text, on_press=on_press, **kwargs)
self.toggle_mode = toggle_mode
self.group = group
self.active = False
self.toggled = Signal()
def _on_gui_input(self, event):
if event.button == 1:
if event.pressed and self.is_point_inside(event.position):
self._is_pressed = True
self.queue_redraw()
self.button_down()
elif not event.pressed and self._is_pressed:
self._is_pressed = False
self.queue_redraw()
self.button_up()
if self.is_point_inside(event.position):
if self.toggle_mode:
# If in a radio group, don't allow deselecting
if self.group and self.active:
return
self.active = not self.active
# Deselect siblings in the same radio group
if self.group and self.active and self.parent:
for child in self.parent.children:
if (
isinstance(child, ToolbarButton)
and child is not self
and child.group == self.group
and child.active
):
child.active = False
child.queue_redraw()
child.toggled(False)
self.toggled(self.active)
self.pressed()
# Colours for active-state tinting (set externally by editor)
active_bg_colour: tuple | None = None
[docs]
def draw(self, renderer):
x, y, w, h = self.get_global_rect()
if self.disabled:
box = self.style_disabled
if box is not None:
box.draw(renderer, x, y, w, h)
if self.text:
scale = self.font_size / 14.0
tw = renderer.text_width(self.text, scale)
renderer.draw_text_coloured(
self.text, x + (w - tw) / 2, y + (h - self.font_size) / 2, scale, self.text_disabled_colour
)
return
# Active state with optional colour override
if self.active and self.active_bg_colour:
renderer.draw_filled_rect(x, y, w, h, self.active_bg_colour)
elif self.active:
box = self.style_pressed
if box is not None:
box.draw(renderer, x, y, w, h)
else:
box = self._get_current_style()
if box is not None:
box.draw(renderer, x, y, w, h)
# Text (centered)
if self.text:
scale = self.font_size / 14.0
text_width = renderer.text_width(self.text, scale)
text_x = x + (w - text_width) / 2
text_y = y + (h - self.font_size) / 2
renderer.draw_text_coloured(self.text, text_x, text_y, scale, self.text_colour)
# ============================================================================
# Toolbar — Horizontal strip of buttons with background bar
# ============================================================================
[docs]
class Toolbar(HBoxContainer):
"""Horizontal toolbar with buttons, separators, and a background bar.
Example:
toolbar = Toolbar()
toolbar.add_button("File")
toolbar.add_separator()
toolbar.add_button("Move", toggle=True, group="tools")
toolbar.add_button("Rotate", toggle=True, group="tools")
"""
toolbar_bg = ThemeColour("toolbar_bg")
toolbar_border = ThemeColour("border")
separator_colour = ThemeColour("border_light")
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.size = Vec2(self.size.x, 36)
self.separation = 2
[docs]
def add_button(self, text: str, icon=None, toggle: bool = False, group: str = "", on_press=None) -> ToolbarButton:
"""Add a toolbar button.
Args:
text: Button label text.
icon: Reserved for future icon support.
toggle: If True, button stays pressed when clicked.
group: Radio group name; clicking deselects other buttons in group.
on_press: Callback invoked when button is clicked.
Returns:
The created ToolbarButton instance.
"""
btn = ToolbarButton(
text=text,
toggle_mode=toggle,
group=group,
on_press=on_press,
)
btn.size = Vec2(max(60, len(text) * 10 + 20), self.size.y - 4)
self.add_child(btn)
return btn
[docs]
def add_separator(self):
"""Add a thin vertical separator line between buttons."""
from .theme import StyleBox
sep = Panel()
sep.size = Vec2(2, self.size.y - 8)
sep.style = StyleBox(bg_colour=self.separator_colour, border_width=0.0)
self.add_child(sep)
[docs]
def draw(self, renderer):
# Draw background bar
x, y, w, h = self.get_global_rect()
renderer.draw_filled_rect(x, y, w, h, self.toolbar_bg)
renderer.draw_line_coloured(x, y + h, x + w, y + h, self.toolbar_border)