Skip to main content

Form Controls

Form control editing is implemented in wgpu-html-tree::text_edit (252 lines) and driven by keyboard input dispatch.

Text Editing

The text_edit module provides pure functions that take current value + cursor and return new value + cursor:

FunctionDescription
insert_text(value, cursor, text)Insert characters at cursor position
delete_forward(value, cursor)Delete character after cursor
delete_backward(value, cursor)Delete character before cursor
move_cursor_left(value, cursor, selection_anchor)Left arrow (with Shift-select)
move_cursor_right(value, cursor, selection_anchor)Right arrow
move_cursor_home(value, cursor, selection_anchor)Home key
move_cursor_end(value, cursor, selection_anchor)End key
insert_line_break(value, cursor)Enter in textareas
select_all(value)Ctrl+A equivalent
delete_selection(value, cursor, selection_anchor)Delete selected range
delete_word_backward(value, cursor)Ctrl+Backspace
delete_word_forward(value, cursor)Ctrl+Delete

All operations are multibyte/UTF-8 safeprev_char() and next_char() navigate by char boundaries using str::is_char_boundary().

Shift+Select

When Shift is held, arrow keys extend the selection rather than moving a collapsed caret:

pub fn move_cursor_right(
value: &str, cursor: usize, selection_anchor: Option<usize>
) -> (usize, Option<usize>)

If selection_anchor is None, it's set to the current cursor position before moving.

Placeholder Rendering

When an <input> or <textarea> has a placeholder attribute but no value, the placeholder text is shaped and painted:

  • Shaped at 50% alpha of the normal text color.
  • Centered vertically in <input> elements.
  • Word-wrapped in <textarea> elements.
  • Excluded from document-level text selection (text_unselectable: true).

Password Masking

<input type="password"> values are displayed as U+2022 (bullet) characters:

let display_value = if is_password {
"\u{2022}".repeat(value.chars().count())
} else {
value.to_string()
};

The shaped display value is used for render only; the underlying value attribute stores the real text.

Blinking Caret

A 1.5px-wide vertical quad blinks on/off every 500ms:

let elapsed = Instant::now().duration_since(tree.interaction.caret_blink_epoch);
let visible = (elapsed.as_millis() / 500) % 2 == 0;

The caret epoch resets on every user interaction (click, keypress) so the caret is always visible immediately after input.

Click-to-Position Caret

When clicking on an empty input (showing placeholder), the caret goes to position 0, not inside the placeholder text. For non-empty fields, glyph-level accuracy is used:

let glyph_idx = run.glyphs.iter()
.position(|g| g.x + g.w * 0.5 > click_x)
.unwrap_or(run.glyphs.len());
let byte_offset = run.byte_boundaries[glyph_idx];

Supported Input Types

TypeBehavior
textStandard text input
passwordBullet-masked text input
emailText input (validation not enforced)
searchText input
telText input
urlText input
numberText input with numeric keyboard hint
date, datetime-local, month, week, timeText input
button, submit, resetRendered as buttons, clickable
Other typesFall back to text behavior

Known Gaps

  • No checkbox/radio toggle: These render as text inputs.
  • No <select> dropdown: The element renders but has no popup menu.
  • No form submission: Submit buttons trigger on_click but don't send data.
  • No input validation (pattern, required, min/max): These attributes are parsed but not enforced.