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 aPointerDown.pointer_id— lets the drag machine reject interleaved events from a second pointer without dropping the primary gesture.delta_x/delta_yon moves — redundant with position differences, but explicitly carried so replay doesn’t need to remember prior state.lineson 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 withschema_versionit 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:
| Error | Trigger |
|---|---|
UnsupportedSchemaVersion | schema_version ≠ current. |
ZeroSequence | sequence == 0. |
ZeroPointerId | Pointer event with pointer_id == 0. |
ZeroWheelLines | WheelNudge { lines: 0 }. |
ZeroResizeUnits | KeyboardResize { 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 PaneOperationEvery 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
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
The state machine that consumes these events.
Drag/resize machinePaneTerminalAdapter and PanePointerCaptureAdapter — the
translators.
Deterministic replay end to end.
Operations and timelineThe lower-level gesture layer that feeds semantic input.
Events and input (core)How this piece fits in panes.
Panes overview