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)