Text Input¶
single-line TextEdit fields with focus routing, submit and activation.
▶ Run in browserTags: ui text input focus scrolling
TextEdit(placeholder="...") is a single-line text field. Click it to give it
the keyboard and type; Enter fires its text_submitted signal with the text,
which here writes “Sent: …” under the fields. A field’s look comes from its
per-state StyleBoxes rather than a flat colour, so each field copies the theme’s
style_normal and style_focused boxes with .replace(...) and changes only
the colours, giving each its own focus border.
Focus moves the way it does in a browser. A TextEdit is in the tab order by
default; a Button takes focus only when clicked until focus_mode=FocusMode.ALL
puts it in the order too. Tab and Shift+Tab then walk username, message and
Send. Enter means whatever the focus owner makes of it: the focused field claims
the key for its own submit, and the focused Send button is pressed exactly as a
click would press it. Only the focus owner is ever activated; there is no
default button.
Traversal and activation are the engine’s ui_focus_next and ui_accept
actions, so they rebind like any other action. F2, or the Traversal button,
calls InputMap.rebind("ui_focus_next", Key.CTRL + Key.TAB) and F3, or the
Activation button, moves ui_accept onto Ctrl+Enter; pressing either again
calls InputMap.clear_rebind to give the engine default back. A rebind replaces
the keyboard binding (slot 0) and leaves the controller’s alone, and each
button’s caption is read back from the map with Input.prompt.
Below the fields a ScrollContainer holds more rows than fit its box: it clips
them and scrolls with the mouse wheel or by dragging its scrollbar.
Source¶
1# /// simvx
2# tags = ["ui", "text", "input", "focus", "scrolling"]
3# web = { width = 800, height = 600, responsive = true }
4# ///
5"""Text Input: single-line TextEdit fields with focus routing, submit and activation.
6
7`TextEdit(placeholder="...")` is a single-line text field. Click it to give it
8the keyboard and type; Enter fires its `text_submitted` signal with the text,
9which here writes "Sent: ..." under the fields. A field's look comes from its
10per-state StyleBoxes rather than a flat colour, so each field copies the theme's
11`style_normal` and `style_focused` boxes with `.replace(...)` and changes only
12the colours, giving each its own focus border.
13
14Focus moves the way it does in a browser. A `TextEdit` is in the tab order by
15default; a `Button` takes focus only when clicked until `focus_mode=FocusMode.ALL`
16puts it in the order too. Tab and Shift+Tab then walk username, message and
17Send. Enter means whatever the focus owner makes of it: the focused field claims
18the key for its own submit, and the focused Send button is pressed exactly as a
19click would press it. Only the focus owner is ever activated; there is no
20default button.
21
22Traversal and activation are the engine's `ui_focus_next` and `ui_accept`
23actions, so they rebind like any other action. F2, or the Traversal button,
24calls `InputMap.rebind("ui_focus_next", Key.CTRL + Key.TAB)` and F3, or the
25Activation button, moves `ui_accept` onto Ctrl+Enter; pressing either again
26calls `InputMap.clear_rebind` to give the engine default back. A rebind replaces
27the keyboard binding (slot 0) and leaves the controller's alone, and each
28button's caption is read back from the map with `Input.prompt`.
29
30Below the fields a `ScrollContainer` holds more rows than fit its box: it clips
31them and scrolls with the mouse wheel or by dragging its scrollbar.
32"""
33
34from simvx.core import (
35 Anchor,
36 Button,
37 Colour,
38 FocusMode,
39 HBoxContainer,
40 Input,
41 InputMap,
42 Key,
43 Label,
44 Node,
45 Panel,
46 ScrollContainer,
47 TextEdit,
48 VBoxContainer,
49 on_input,
50)
51from simvx.graphics import App
52
53FIELD_BG = Colour.hex("#1A1A2E")
54# The key each engine action moves to when rebound.
55ALTERNATIVE = {"ui_focus_next": Key.CTRL + Key.TAB, "ui_accept": Key.CTRL + Key.ENTER}
56
57
58class TextInputDemo(Node):
59 input_context = {"rebind_traversal": [Key.F2], "rebind_activation": [Key.F3]}
60
61 def on_ready(self):
62 panel = self.add_child(Panel(anchor=Anchor.FULL_RECT, inset=30))
63 panel.bg_colour = Colour.hex("#12121A")
64 column = panel.add_child(VBoxContainer(anchor=Anchor.FULL_RECT, inset=20, separation=8))
65
66 title = column.add_child(Label("Text Input & Clipping", font_size=16, alignment="centre"))
67 title.text_colour = Colour.hex("#4FC3F7")
68
69 column.add_child(Label("Username:"))
70 self.username = column.add_child(field("Enter username...", Colour.hex("#4FC3F7")))
71 column.add_child(Label("Message:"))
72 self.message = column.add_child(field("Type a message...", Colour.hex("#E94560")))
73 self.message.text_submitted.connect(self.send)
74
75 self.echo = column.add_child(Label())
76 self.echo.text_colour = Colour.GREEN
77 self.send_button = column.add_child(
78 Button("Send", focus_mode=FocusMode.ALL, size=(0, 30), on_press=lambda: self.send(self.message.text))
79 )
80
81 column.add_child(Label("Scrollable area (mouse wheel, or drag the bar):"))
82 self.scroll = column.add_child(ScrollContainer(size=(0, 160), separation=4))
83 for i in range(1, 11):
84 self.scroll.add_child(Label(f"Line {i}: scrollable content row {i}"))
85
86 rebinds = column.add_child(HBoxContainer(separation=12))
87 self.traversal = rebinds.add_child(Button(on_press=lambda: self.toggle("ui_focus_next")))
88 self.activation = rebinds.add_child(Button(on_press=lambda: self.toggle("ui_accept")))
89 self.show_bindings()
90
91 # Example help overlay: not part of the technique shown.
92 help_text = "Traversal walks the fields and Send / Activation submits or presses / F2, F3 rebind"
93 hint = column.add_child(Label(help_text))
94 hint.text_colour = Colour.GRAY
95
96 def send(self, text: str):
97 self.echo.text = f"Sent: {text}"
98
99 @on_input("rebind_traversal")
100 def rebind_traversal(self, event):
101 self.toggle("ui_focus_next")
102
103 @on_input("rebind_activation")
104 def rebind_activation(self, event):
105 self.toggle("ui_accept")
106
107 def toggle(self, action: str):
108 toggle_rebind(action)
109 self.show_bindings()
110
111 def show_bindings(self):
112 self.traversal.text = f"Traversal: {Input.prompt('ui_focus_next')} (F2)"
113 self.activation.text = f"Activation: {Input.prompt('ui_accept')} (F3)"
114
115
116def field(placeholder: str, focus_colour: Colour) -> TextEdit:
117 """A text field with its own background and focus border."""
118 edit = TextEdit(placeholder=placeholder)
119 edit.style_normal = edit.style_normal.replace(bg_colour=FIELD_BG)
120 edit.style_focused = edit.style_focused.replace(bg_colour=FIELD_BG, border_colour=focus_colour)
121 return edit
122
123
124def toggle_rebind(action: str):
125 """Move an engine action onto its alternative key, or give it back its default."""
126 if any(rebind.action == action for rebind in InputMap.rebinds):
127 InputMap.clear_rebind(action)
128 else:
129 InputMap.rebind(action, ALTERNATIVE[action])
130
131
132if __name__ == "__main__":
133 App(title="Text Input", width=800, height=600).run(TextInputDemo())