Rotated Controls¶
widgets turn, scale and take clicks along with their parent.
▶ Run in browserTags: ui transform rotation layout
Every Control is a Node2D, so it has a rotation and a scale, and so does
everything above it. A widget draws in its own local space, which means turning
or scaling a parent turns and scales every widget beneath it: here a settings
card of buttons, a Slider, a CheckBox and a TextEdit spins and grows as
one piece. Hit-testing runs the same transform backwards, mapping each pointer
press into the widget’s local space, so a click lands on the widget drawn under
the pointer however far the card has turned, and the slider reads the press
along its own tilted axis.
A control rotates about its own origin, its top-left corner. To spin the card
about its middle, the scene puts an empty, zero-sized Control anchored at the
window’s CENTER as a pivot and hangs the card off it, itself CENTER-anchored,
so the card’s middle sits on the pivot’s origin. The pivot carries the rotation and
the scale and the anchors keep it centred at any window size.
The card is live while it spins: the Pause toggle button (toggle_mode=True)
holds the spin while it is checked, - and + rescale the card, the slider
updates the volume label through its value_changed signal, and the check box
and text field work as they would upright.
Source¶
1# /// simvx
2# tags = ["ui", "transform", "rotation", "layout"]
3# ///
4"""Rotated Controls: widgets turn, scale and take clicks along with their parent.
5
6Every `Control` is a `Node2D`, so it has a `rotation` and a `scale`, and so does
7everything above it. A widget draws in its own local space, which means turning
8or scaling a parent turns and scales every widget beneath it: here a settings
9card of buttons, a `Slider`, a `CheckBox` and a `TextEdit` spins and grows as
10one piece. Hit-testing runs the same transform backwards, mapping each pointer
11press into the widget's local space, so a click lands on the widget drawn under
12the pointer however far the card has turned, and the slider reads the press
13along its own tilted axis.
14
15A control rotates about its own origin, its top-left corner. To spin the card
16about its middle, the scene puts an empty, zero-sized `Control` anchored at the
17window's `CENTER` as a pivot and hangs the card off it, itself `CENTER`-anchored,
18so the card's middle sits on the pivot's origin. The pivot carries the rotation and
19the scale and the anchors keep it centred at any window size.
20
21The card is live while it spins: the Pause toggle button (`toggle_mode=True`)
22holds the spin while it is checked, `-` and `+` rescale the card, the slider
23updates the volume label through its `value_changed` signal, and the check box
24and text field work as they would upright.
25"""
26
27from simvx.core import (
28 Anchor,
29 Button,
30 CheckBox,
31 Colour,
32 Control,
33 HBoxContainer,
34 Label,
35 Node,
36 Panel,
37 Slider,
38 TextEdit,
39 VBoxContainer,
40)
41from simvx.graphics import App
42
43SPIN_SPEED = 0.35 # radians per second
44SCALE_STEP = 0.15
45SCALE_MIN, SCALE_MAX = 0.6, 1.8
46
47
48class RotatedControlsDemo(Node):
49 """A centred pivot carrying a card of live widgets."""
50
51 def on_ready(self):
52 self.pivot = self.add_child(Control(name="Pivot", anchor=Anchor.CENTER, size=(0, 0)))
53 card = self.pivot.add_child(Panel(name="Card", anchor=Anchor.CENTER, size=(340, 250)))
54 card.bg_colour = Colour.hex("#1E2233")
55
56 column = card.add_child(VBoxContainer(name="Column", anchor=Anchor.FULL_RECT, inset=(16, 14), separation=10.0))
57 title = column.add_child(Label("Settings", font_size=18.0))
58 title.text_colour = Colour.hex("#FFD166")
59
60 row = column.add_child(HBoxContainer(separation=8.0))
61 self.pause = row.add_child(Button("Pause", name="Pause", toggle_mode=True))
62 row.add_child(Button("-", name="Smaller", on_press=lambda: self.rescale(-SCALE_STEP)))
63 row.add_child(Button("+", name="Bigger", on_press=lambda: self.rescale(SCALE_STEP)))
64
65 self.volume_label = column.add_child(Label("Volume: 40", name="VolumeLabel"))
66 volume = column.add_child(Slider(name="Volume", min_value=0, max_value=100, value=40))
67 volume.value_changed.connect(self.show_volume)
68 column.add_child(CheckBox("V-sync", name="VSync", checked=True))
69 column.add_child(TextEdit(name="PlayerName", placeholder="Player name"))
70
71 def rescale(self, step: float):
72 s = min(SCALE_MAX, max(SCALE_MIN, float(self.pivot.scale.x) + step))
73 self.pivot.scale = (s, s)
74
75 def show_volume(self, value: float):
76 self.volume_label.text = f"Volume: {int(value)}"
77
78 def on_update(self, dt: float):
79 if not self.pause.checked:
80 self.pivot.rotation += SPIN_SPEED * dt
81
82
83if __name__ == "__main__":
84 App(title="SimVX Rotated Controls", width=800, height=600).run(RotatedControlsDemo())