Source code for simvx.core.ui.toolbar

"""Toolbar: horizontal button strip with toggle/radio group support."""

import logging

from ..descriptors import Property
from ..input.enums import MouseButton
from ..math.types import Vec2
from ..signals import Signal
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_mode=True, group="tools") btn.toggled.connect(lambda active: print(f"active={active}")) """ toggle_mode = Property(False, hint="Behave as a toggle rather than a push button") group = Property("", hint="Radio-group name; members deselect each other") active = Property(False, hint="Toggle state") def __init__(self, text: str = "Button", on_press=None, **kwargs): super().__init__(text=text, on_press=on_press, **kwargs) self.toggled = Signal()
[docs] def activate(self) -> bool: """Apply the toggle / radio-group state, then fire ``pressed``. Overridden so the ``ui_accept`` key does the whole of what a click does: the base ``Button`` only fires ``pressed``, which on a toggle button would leave ``active`` behind. """ if self.toggle_mode: # In a radio group the active member cannot be deselected: the key is # still consumed, it simply has nothing to change. if self.group and self.active: return True 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.toggled(False) self.toggled(self.active) self.pressed() return True
def _on_gui_input(self, event): # Claimed when it acts: the click pressed this tool, not the bar behind it. if event.button == MouseButton.LEFT: if event.pressed and self.is_point_inside(event.position): self._is_pressed = True self.queue_redraw() self.button_down() event.handled = True 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): self.activate() event.handled = True # Colours for active-state tinting (set externally by editor) active_bg_colour: tuple | None = None
[docs] def on_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 / 16.0 tw = renderer.text_width(self.text, scale) renderer.draw_text( self.text, (x + (w - tw) / 2, y + (h - self.font_size) / 2), colour=self.text_disabled_colour, scale=scale, ) return # Active state with optional colour override if self.active and self.active_bg_colour: renderer.draw_rect((x, y), (w, h), colour=self.active_bg_colour, filled=True) 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 / 16.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(self.text, (text_x, text_y), colour=self.text_colour, scale=scale)
# ============================================================================ # 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") separation = Property(2.0, range=(0, 100), hint="Space between toolbar items", on_change="mark_layout_dirty") size_y = Property(36.0, range=(0, 10000), hint="Control height", on_change="_on_size_changed")
[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 on_draw(self, renderer): # Draw background bar x, y, w, h = self.get_global_rect() renderer.draw_rect((x, y), (w, h), colour=self.toolbar_bg, filled=True) renderer.draw_line((x, y + h), (x + w, y + h), colour=self.toolbar_border)