Skip to Content
ftui-layoutPanesSemantic input

Semantic Input

Terminals and browsers do not speak the same input language. A TTY delivers SGR-mouse CSI sequences; a browser dispatches pointermove / pointerup / wheel events with sub-pixel precision. If the pane system consumed raw host events, terminal/web parity would be a polite fiction.

PaneSemanticInputEvent is the translated, host-agnostic vocabulary that actually feeds the pane machinery. Every backend lowers its native input to this type. After that point, pane logic is identical everywhere.

The shape

pub struct PaneSemanticInputEvent { pub schema_version: u16, pub sequence: u64, // monotonic, non-zero pub modifiers: PaneModifierSnapshot, // shift/alt/ctrl/meta pub kind: PaneSemanticInputEventKind, pub extensions: BTreeMap<String, String>, // forward-compat bag }

Schema-versioned, monotonically sequenced, modifiers always present, forward-compatible. These four properties are the whole reason the type exists.

The seven event kinds

pub enum PaneSemanticInputEventKind { PointerDown { target: PaneResizeTarget, pointer_id: u32, // must be non-zero button: PanePointerButton, position: PanePointerPosition, }, PointerMove { target: PaneResizeTarget, pointer_id: u32, position: PanePointerPosition, delta_x: i32, delta_y: i32, }, PointerUp { target: PaneResizeTarget, pointer_id: u32, button: PanePointerButton, position: PanePointerPosition, }, WheelNudge { target: PaneResizeTarget, lines: i16, // must be non-zero }, KeyboardResize { target: PaneResizeTarget, direction: PaneResizeDirection, units: u16, // must be non-zero }, Cancel { target: Option<PaneResizeTarget>, reason: PaneCancelReason, }, Blur { target: Option<PaneResizeTarget>, }, }

Notice what is not here: coordinate precision modes, OS-specific pointer types, or differentiated wheel modes. Those are the host’s problem.

Why each field exists

  • target: PaneResizeTarget — identifies which split or grip the event refers to. { split_id, axis } in the common case. If you don’t know (a loose pointer event not over a grip), you never emit a PointerDown.
  • pointer_id — lets the drag machine reject interleaved events from a second pointer without dropping the primary gesture.
  • delta_x / delta_y on moves — redundant with position differences, but explicitly carried so replay doesn’t need to remember prior state.
  • lines on wheel — positive means “grow this pane,” negative means “shrink.” The magnetic-field radius is adjusted by wheel ticks in some UI modes; see magnetic docking.
  • sequence — a strictly monotonic u64 across the whole session. Paired with schema_version it uniquely addresses any event in any recording.

Validation

Every event is validatable in isolation:

pub fn validate(&self) -> Result<(), PaneSemanticInputEventError>;

The failure modes are all trivially checked:

ErrorTrigger
UnsupportedSchemaVersionschema_version ≠ current.
ZeroSequencesequence == 0.
ZeroPointerIdPointer event with pointer_id == 0.
ZeroWheelLinesWheelNudge { lines: 0 }.
ZeroResizeUnitsKeyboardResize { units: 0 }.

sequence = 0 is reserved as the “nothing has happened yet” value, and pointer_id = 0 is reserved as “no pointer.” Everything else is a validation bug on the host side and should be caught in testing.

The flow — host to operation

┌────────────────────┐ ┌─────────────────────────────┐ ┌──────────────────────┐ ┌───────────────────┐ │ host input │ │ host adapter │ │ PaneDragResizeMachine│ │ PaneTree │ │ (TTY CSI / DOM │──▶│ PaneTerminalAdapter | │──▶│ (Idle → Armed → │──▶│ apply_operation() │ │ pointer events) │ │ PanePointerCaptureAdapter │ │ Dragging) │ │ │ └────────────────────┘ └─────────────────────────────┘ └──────────────────────┘ └───────────────────┘ raw events PaneSemanticInputEvent PaneDragResizeEffect PaneOperation

Every step is replay-friendly. You can record at any arrow and replay downstream deterministically.

Traces — serialize a session

pub struct PaneSemanticInputTrace { pub events: Vec<PaneSemanticInputEvent>, pub metadata: PaneSemanticInputTraceMetadata, /* checksum, etc. */ } impl PaneSemanticInputTrace { pub fn new(events: Vec<PaneSemanticInputEvent>, /* … */) -> Self; pub fn recompute_checksum(&self) -> u64; pub fn validate(&self) -> Result<(), PaneSemanticInputTraceError>; pub fn replay(&self, /* … */) -> /* … */; }

A trace is a checksummed log of events plus metadata (start time, client ID, platform identifier). Replaying a trace against a backend and comparing state_hash() after each event is how cross-backend conformance tests assert parity. See terminal/web parity.

Worked example — build one by hand

synthetic_click.rs
use ftui_layout::pane::{ PaneModifierSnapshot, PanePointerButton, PanePointerPosition, PaneResizeTarget, PaneSemanticInputEvent, PaneSemanticInputEventKind, SplitAxis, }; let target = PaneResizeTarget { split_id: ftui_layout::pane::PaneId::new(3)?, axis: SplitAxis::Vertical, }; let down = PaneSemanticInputEvent::new( /* sequence = */ 1, PaneSemanticInputEventKind::PointerDown { target, pointer_id: 1, button: PanePointerButton::Primary, position: PanePointerPosition::new(40, 12), }, ); down.validate()?;

Sequence monotonicity across a gesture is your job — each subsequent event should bump sequence. Traces enforce this at construction.

Pitfalls

Don’t reuse sequence. Even if an event is “almost the same” as the previous one, give it a new sequence. Trace replay requires strict monotonicity to locate divergence.

pointer_id and sequence are different. pointer_id identifies the physical pointer across a gesture (so both hands on a trackpad don’t collide). sequence identifies the event in the trace. They never coincide.

Cancel and Blur are not the same. Cancel has a PaneCancelReason — the user pressed Escape, or the target disappeared. Blur is “the window lost focus”; the drag is suspended in place. Adapters must pick the right one.

Where to go next