Rotated Controls

widgets turn, scale and take clicks along with their parent.

▶ Run in browser

Tags: 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())