Text Input

single-line TextEdit fields with focus routing, submit and activation.

▶ Run in browser

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