///|
/// 三条输入通道:终端事实 → 宿主事件,一条都不吞。
///
/// 分工按「谁可能知道这个事实」切开:
/// - `decode` 只报终端能表达的:cell 坐标、滚轮一格、原始按钮号、修饰键位;
/// - 这里只补终端不知道的:cell→逻辑像素、按钮编号对照、宿主事件外壳;
/// - 派发时机在 `terminal_host`:每条事实立刻走一次 update→present,
/// 所以「按下即响应」不依赖 Enter 或提交。
///
/// 键名与文本分两条事件,和桌面端「按键 + IME 提交」同形:键盘通道只报哪个键、
/// 什么状态,插入只由文本通道报。两边都带文本会让焦点文本框把同一个字符插两遍
/// (它读 `event.text`,见 `moui/views/text/text_input_event_handlers.mbt:234`)。
pub struct TerminalInput {
input : @window_input.InputState
surface : @window_input.SurfaceState
cell : @tui_renderer.CellBox
}
///|
/// SGR 的「无键移动」按钮号:1003 模式下拖动位置位、按钮号为 3,
/// 表示当前没有键按下,不是第 4 个按钮。
const NO_SGR_BUTTON : Int = 3
///|
/// 按钮编号对照:终端是 0 左 1 中 2 右,MoUI 核心是 0 左 1 右 2 中
/// (`moui/backend/common/input/window_event_decode.mbt:127`)。
/// 直接透传会把中键和右键互换。
fn sgr_button_to_core(button : Int) -> Int {
match button {
1 => 2
2 => 1
_ => button
}
}
///|
/// 终端网格 → 逻辑像素视口。终端没有 DPR,缩放恒为 1:再乘一层只会让
/// 「一格滚轮」和「一格点击」变成需要解释的东西。
fn grid_metrics(
cols : Int,
rows : Int,
cell : @tui_renderer.CellBox,
) -> @core.SurfaceMetrics {
@core.SurfaceMetrics::from_logical(
size=@core.Size::new(
width=cols.to_double() * cell.width,
height=rows.to_double() * cell.height,
),
scale_factor=1.0,
)
}
///|
/// 建立输入通道。`cols`/`rows` 来自 `Tty::grid`,所以构造即已知视口尺寸;
/// 宿主必须先用 `metrics` 造 HostSurface,再去绑渲染器——尺寸为 0 的 surface
/// 会被共享的 bind gate 拒绝。
pub fn TerminalInput::new(
cols~ : Int,
rows~ : Int,
cell? : @tui_renderer.CellBox = @tui_renderer.CellBox::default(),
) -> TerminalInput {
{
input: @window_input.InputState::new(
// raw mode 期间本进程独占键盘,没有「窗口失焦」这种事实可报,
// 所以文本通道从第一步就放行;焦点落在哪个控件由运行时决定。
focused=true,
contract=@backend_host.HostInputContract::new(
capabilities=@backend_host.HostInputCapabilities::new(
pointer=true,
scroll=true,
keyboard=true,
focus=false,
text_input=true,
),
coordinates=@backend_host.HostCoordinatePolicy::logical(
description="Cell centres scaled by the current cell box.",
),
),
),
surface: @window_input.SurfaceState::new(
metrics=grid_metrics(cols, rows, cell),
owns_surface=true,
),
cell,
}
}
///|
/// 当前视口度量:宿主用它建 HostSurface,并在 resize 后交给渲染会话重排。
pub fn TerminalInput::metrics(self : TerminalInput) -> @core.SurfaceMetrics {
self.surface.metrics
}
///|
/// 尺寸通道:一次 resize 产出一条新的视口事实。宿主拿到它之后还要做两件事
/// ——让渲染会话按新度量重画,让运行时走完整的 update→layout→present。
pub fn TerminalInput::resize(
self : TerminalInput,
cols : Int,
rows : Int,
) -> @backend_host.Event {
self.surface.resize(grid_metrics(cols, rows, self.cell))
}
///|
/// cell 坐标 → 逻辑像素,取格心。取左上角会让「正好落在格子边界」的盒差一格;
/// 格心恒在自己量化盒内部,末行末列也不会越出视口。
pub fn TerminalInput::point_of(
self : TerminalInput,
position : CellPosition,
) -> @core.Point {
@core.Point::new(
x=(position.col.to_double() + 0.5) * self.cell.width,
y=(position.row.to_double() + 0.5) * self.cell.height,
)
}
///|
/// 一条终端事实换算成宿主事件。`None` 只发生在「这条事实不该进入应用」:
/// 按钮号与相位矛盾(无键却报按下/释放),或能力表关掉了某条通道。
pub fn TerminalInput::to_host(
self : TerminalInput,
event : TerminalEvent,
) -> @backend_host.Event? {
match event {
Key(key) => self.keyboard(key)
Text(text) => self.input.text_event(text)
Mouse(mouse) => self.pointer(mouse)
Wheel(wheel) => Some(self.wheel(wheel))
}
}
///|
fn TerminalInput::keyboard(
self : TerminalInput,
key : @core.KeyboardEvent,
) -> @backend_host.Event? {
// 修饰键是这一次按键的事实,而 `keyboard_event` 盖的是当前状态,所以先落地。
self.input.set_modifiers(key.modifiers)
self.input.keyboard_event(key=key.key, pressed=key.pressed, repeat=key.repeat)
}
///|
fn TerminalInput::pointer(
self : TerminalInput,
mouse : MouseReport,
) -> @backend_host.Event? {
let position = self.point_of(mouse.position)
let modifiers = mouse.modifiers
match mouse.phase {
Drag =>
// 拖动只报「位置变了」,是否算拖拽由运行时的指针捕获状态决定;
// 桌面端也这么发(window_event_decode.mbt:223)。
Some(Pointer(@core.PointerEvent::new(position~, phase=Move, modifiers~)))
Press if mouse.button != NO_SGR_BUTTON =>
Some(
Pointer(
@core.PointerEvent::new(
position~,
phase=Down,
button=sgr_button_to_core(mouse.button),
modifiers~,
),
),
)
Release if mouse.button != NO_SGR_BUTTON =>
Some(
Pointer(
@core.PointerEvent::new(
position~,
phase=Up,
button=sgr_button_to_core(mouse.button),
modifiers~,
),
),
)
_ => None
}
}
///|
fn TerminalInput::wheel(
self : TerminalInput,
wheel : WheelReport,
) -> @backend_host.Event {
// 一格滚轮 = 一行:增量按当前 cell 高度换算。不走 `Scroll` 包装,因为那条线
// 把行高写死成 40 像素并丢掉修饰键(`moui/backend/input_host_state.mbt:150`),
// 而终端两个事实都要保住。正向 y 让内容下移(`text_area_viewport.mbt:88`),
// 所以向上滚给正增量。
let step = if wheel.up { self.cell.height } else { -self.cell.height }
Pointer(
@core.PointerEvent::new(
position=self.point_of(wheel.position),
phase=Move,
wheel_delta=@core.Point::new(x=0.0, y=step),
modifiers=wheel.modifiers,
),
)
}