diff --git a/agents.md b/agents.md index 7213c1e..50a2ded 100644 --- a/agents.md +++ b/agents.md @@ -305,4 +305,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-15(用户报:ElyMark 源码模式“测”后的插入点画在“测”的中间):组件库 `CodeEditor` 原按“列 × m 的宽”放插入点、选区、按下处与输入法框,中日韩字与表情约宽两格。组件库 9fb607b 起按 gpui 排好的这一行定位(`editor::measure`,含着色带来的字重),块状与下划线插入点取所在字的宽;标尺与缩进参考线仍按列(R6.8)。上下移动仍按字数记目标列。cd61653:按下处取最近的字首或行末(gpui 的 `closest_index_for_x` 越过末字字首即给行末);上次绘制得出的括号与配对位置只属当时的文字(`Buffer` 每次改动换一个戳),输入法改字后、下次绘制前的测量不用它们(原先把新字切进不同字体段,DirectWrite 崩溃)。 - 2026-10-15(用户报:ElyMark 三、四级标题不显眼):组件库 5954132 起标题字号随正文按 Typora 的倍数(一至六级 2.25、1.75、1.5、1.25、1、1),一律加粗,六级为次要色,行高为字号的 1.3 倍(原为固定的 24、20、16、14 像素,正文 13,三、四级只是中等字重)(R6.9)。90bd37b:粗体与标题同一字重(原粗体为半粗,标题里的粗体反而更细,正文粗体随之由半粗改为粗,同 Typora);标题行高按自身字号取 1.3 倍。Windows 真窗口核对(`.work/r69`):可视模式各级标题分明、“你好↵测试”两行,源码模式“测”后的插入点落在两字之间。 - 2026-10-15(用户报:macOS 打开一个文档,程序坞里有两个同样的图标):LOK worker 在应用包的 `Contents/MacOS` 里启动 AppKit,系统按包的 Info.plist 把它也登记为常规应用(`lsappinfo` 里应用与 worker 都是 Foreground),程序坞多一个图标;内核自己从不设激活策略。worker 在内核启动 AppKit 之前(`Lok::hook` 打开内核之后、调钩子之前)把本进程主包信息里的 `LSUIElement` 设为真,AppKit 登记时即为辅助应用(无程序坞图标、无菜单栏,仍可建屏外窗口);钩子之后核对激活策略是辅助,不是即报错、不启动(`ffi/accessory.rs`);包外的 worker(`cargo run`、单测)无包可登记,策略为禁止,此时改设辅助(R6.12 第 2 轮)。原先在登记之后才改策略,审查实测 15 次启动有 11 次先登记为 Foreground 约 30 毫秒(R6.12 第 1 轮)。`tests/scripts/dock-macos.sh` 连续启动三个应用与包外的开发构建,从 worker 启动起采样其类型到首块图块,见 Foreground 即失败(R6.12)。 -- 2026-10-15(用户报:ElyShow 编辑区滚轮与上下键不换页,三端):内核在未选中对象时把上下键当作滚动窗口(`fupoor.cxx`),Ely 的画布整页适合、无处可滚,滚轮同样。`LokView::turn_parts`(ElyShow 首次适合时调用)之后:整页可见时滚轮换页(`view/turn.rs`:每格一页;触控板一次手势只换一页,惯性不算,静止 250 毫秒或新手势再计),未选中对象、未编辑文字时上下键换页;视图发 `Turn`,ElyShow 从视图所示的页起算(连续换页累加),经缩略图条同一路径 `show_part`,到首末页即停;后台读回的当前页只在视图已无待落地输入时采用,排队的换页不被先前的读取拉回(R6.13)。 +- 2026-10-15(用户报:ElyShow 编辑区滚轮与上下键不换页,三端):内核在未选中对象时把上下键当作滚动窗口(`fupoor.cxx`),Ely 的画布整页适合、无处可滚,滚轮同样。`LokView::turn_parts`(ElyShow 首次适合时调用)之后:整页可见时滚轮换页(`view/turn.rs`):滚轮按本平台每格的行数(`ely_lok::notch`:macOS 1,Windows 取系统设置,X11 与 Wayland 为 3)计格,每格一页;不足一格的行(X11 与 Windows 的触控板)与像素(macOS、Wayland 的触控板)一次手势只换一页,惯性不算;有起止的手势(macOS)只到结束才算完,没有的静止 250 毫秒再计。上下键经有序输入队列交 worker(`TURN_KEY`),worker 按之前各输入回报后的状态判定:无选区、无选中对象、无插入点即换页,否则照常交内核(Tab 之后紧接的 Down 移动所选对象,不换页);视图发 `Turn`,ElyShow 从视图所示的页起算(连续换页累加),经缩略图条同一路径 `show_part`,到首末页即停;后台读回的当前页只在视图已无待落地输入时采用,排队的换页不被先前的读取拉回(R6.13)。 diff --git a/apps/ely-show/src/tests/turn.rs b/apps/ely-show/src/tests/turn.rs index 4de7bf3..03d4447 100644 --- a/apps/ely-show/src/tests/turn.rs +++ b/apps/ely-show/src/tests/turn.rs @@ -2,7 +2,7 @@ use ely_lok::LokView; use ely_office::prelude::*; use gpui::{Entity, ScrollDelta, ScrollWheelEvent, TestAppContext, VisualTestContext, point}; -use super::objects::blank; +use super::objects::{blank, selection}; use super::{counts, quiet, settle, update}; use crate::deck::Deck; @@ -22,9 +22,10 @@ fn current(shown: &mut VisualTestContext, deck: &Entity) -> usize { shown.update(|_, app| deck.read(app).active_tab().map(|tab| tab.extra.slides.current).expect("a tab")) } -fn wheel(shown: &mut VisualTestContext, lines: f32) { +/// Whole notches, in this platform's lines. +fn wheel(shown: &mut VisualTestContext, notches: f32) { let middle = shown.update(|window, _| window.viewport_size()).center(); - let delta = ScrollDelta::Lines(point(0.0, lines)); + let delta = ScrollDelta::Lines(point(0.0, notches * ely_lok::notch())); shown.simulate_event(ScrollWheelEvent { position: middle.into(), delta, ..Default::default() }); } @@ -59,4 +60,8 @@ fn the_wheel_and_up_down_turn_slides() { assert_eq!(current(&mut shown, &deck), 1, "Up turns back"); wheel(&mut shown, 1.0); assert_eq!(current(&mut shown, &deck), 0, "the wheel turns up"); + // Down queued behind Tab moves the object Tab picks. + shown.simulate_keystrokes("tab down"); + assert_eq!(current(&mut shown, &deck), 0, "Down after Tab moves, not turns"); + assert!(shown.update(|_, app| selection(app, &deck)).is_some(), "Tab picked an object"); } diff --git a/crates/ely-lok/src/client.rs b/crates/ely-lok/src/client.rs index 133bbec..5b82508 100644 --- a/crates/ely-lok/src/client.rs +++ b/crates/ely-lok/src/client.rs @@ -237,6 +237,14 @@ impl Worker { Ok(()) } + /// Up or Down: whether it turned, not reaching the kernel. + pub fn turn_key(&mut self, doc: &Doc, char_code: u32, key_code: i32) -> io::Result { + let doc_id = self.doc_id(doc)?; + let payload = Pack::new().u64(doc_id).i64(char_code as i64).i64(key_code as i64).bytes(); + let reply = self.request(tag::TURN_KEY, &payload)?; + Ok(Unpack::new(&reply).u64()? == 1) + } + /// Drags a selected object's handle, start then end. pub fn drag_graphic(&mut self, doc: &Doc, from: (i32, i32), to: (i32, i32)) -> io::Result<()> { for (kind, (x, y)) in [(0, from), (1, to)] { diff --git a/crates/ely-lok/src/events.rs b/crates/ely-lok/src/events.rs index 2e5928b..2259903 100644 --- a/crates/ely-lok/src/events.rs +++ b/crates/ely-lok/src/events.rs @@ -362,11 +362,14 @@ pub fn stop_all(docs: &HashMap) { /// The current generation of `id`. pub fn generation(id: u64) -> u64 { - slots() - .lock() - .expect("the events lock") - .get(&id) - .map_or(0, |slot| slot.batch.generation) + slots().lock().expect("the events lock").get(&id).map_or(0, |slot| slot.batch.generation) +} + +/// Whether `id` shows a selection, object or caret. +pub fn picked(id: u64) -> bool { + let slots = slots().lock().expect("the events lock"); + let Some(batch) = slots.get(&id).map(|slot| &slot.batch) else { return false }; + batch.graphic.is_some() || !batch.selection.is_empty() || batch.cursor_visible && batch.cursor.is_some() } /// Blocks until documents have unpushed events. @@ -399,12 +402,7 @@ pub fn clear_result(id: u64) { /// The completion the last dispatch reported. pub fn take_result(id: u64) -> Option<(String, bool)> { - slots() - .lock() - .expect("the events lock") - .get_mut(&id)? - .result - .take() + slots().lock().expect("the events lock").get_mut(&id)?.result.take() } /// The bridged a11y text now. diff --git a/crates/ely-lok/src/ipc.rs b/crates/ely-lok/src/ipc.rs index 4dc2cea..5d90257 100644 --- a/crates/ely-lok/src/ipc.rs +++ b/crates/ely-lok/src/ipc.rs @@ -88,6 +88,8 @@ pub mod tag { pub const REVISION: u8 = 34; /// One layer's pixels from the last `SLIDE_LAYERS`. pub const SLIDE_LAYER: u8 = 35; + /// Up or Down: turns the part when nothing is picked. + pub const TURN_KEY: u8 = 36; pub const OK: u8 = 100; pub const ERR: u8 = 101; } diff --git a/crates/ely-lok/src/lib.rs b/crates/ely-lok/src/lib.rs index 83f370a..c22b8ad 100644 --- a/crates/ely-lok/src/lib.rs +++ b/crates/ely-lok/src/lib.rs @@ -20,7 +20,7 @@ pub use client::{ }; #[cfg(feature = "view")] pub use view::{ - CONTEXT, Copy, Cut, Geom, Layout, LokError, LokView, Paste, Reported, Turn, ZOOM_STEPS, + CONTEXT, Copy, Cut, Geom, Layout, LokError, LokView, Paste, Reported, Turn, ZOOM_STEPS, notch, Zoomed, init, layout, paper, read_off, scheme, }; #[cfg(feature = "view")] diff --git a/crates/ely-lok/src/view.rs b/crates/ely-lok/src/view.rs index 23ee2d8..5e2cce1 100644 --- a/crates/ely-lok/src/view.rs +++ b/crates/ely-lok/src/view.rs @@ -34,7 +34,7 @@ pub use input::{CONTEXT, Copy, Cut, Paste, init}; pub use off::read_off; pub use render::{paper, scheme}; mod turn; -pub use turn::Turn; +pub use turn::{Turn, notch}; pub(crate) use crate::{TILE_TWIPS, TWIPS_PER_PIXEL}; diff --git a/crates/ely-lok/src/view/input.rs b/crates/ely-lok/src/view/input.rs index dd227e0..21a90d6 100644 --- a/crates/ely-lok/src/view/input.rs +++ b/crates/ely-lok/src/view/input.rs @@ -64,6 +64,18 @@ pub(super) enum Input { Visible(Rect, (u32, i32)), /// Shows sheet or slide `part`, from 0. Part(usize), + /// Up or Down; turns `step` parts when nothing is picked. + Turn { + char_code: u32, + key_code: i32, + step: i32, + }, +} + +/// What an input gives back. +pub(super) enum Back { + Copied(String), + Turned(i32), } impl Input { @@ -73,6 +85,7 @@ impl Input { // Arrows, Home, End, Page keys, Escape. Input::Key { key_code, .. } => !matches!(key_code & 0x0FFF, 1024..=1031 | 1281), Input::Mouse(_) + | Input::Turn { .. } | Input::Copy { cut: false } | Input::Visible(..) | Input::Part(_) @@ -169,7 +182,7 @@ fn lok_button(button: MouseButton) -> Option { } /// Runs one input; gives a copy's mark and the revision. -pub(super) fn run(worker: &Mutex, doc: &Doc, input: Input) -> (io::Result>, Option) { +pub(super) fn run(worker: &Mutex, doc: &Doc, input: Input) -> (io::Result>, Option) { let mut worker = worker.lock().expect("the worker lock"); let counts = !matches!(input, Input::Visible(..)); let copied = apply(&mut worker, doc, input); @@ -181,12 +194,16 @@ pub(super) fn run(worker: &Mutex, doc: &Doc, input: Input) -> (io::Resul (copied, now) } -fn apply(worker: &mut Worker, doc: &Doc, input: Input) -> io::Result> { +fn apply(worker: &mut Worker, doc: &Doc, input: Input) -> io::Result> { let done = match input { Input::Key { char_code, key_code, } => worker.post_key(doc, char_code, key_code), + Input::Turn { char_code, key_code, step } => { + let turned = worker.turn_key(doc, char_code, key_code)?; + return Ok(turned.then_some(Back::Turned(step))); + } Input::Mouse(event) => worker.post_mouse(doc, event), Input::Drag { from, to } => worker.drag_graphic(doc, from, to), Input::Visible(area, scale) => doc.visible_area(worker, area, scale), @@ -195,9 +212,8 @@ fn apply(worker: &mut Worker, doc: &Doc, input: Input) -> io::Result worker.run(doc, "view.theme", &[scheme]), Input::Text { commit, text } => worker.post_text(doc, commit, &text), Input::Copy { cut } => { - return clip::copy_out(worker, doc, cut, |clip| { - clipboard::write(clip).map_err(io::Error::other) - }); + let copied = clip::copy_out(worker, doc, cut, |clip| clipboard::write(clip).map_err(io::Error::other)); + return copied.map(|copied| copied.map(Back::Copied)); } Input::Run { commands, at } => { let values: Vec> = @@ -299,7 +315,7 @@ impl LokView { cx: &mut Context, ) { if self.walk(&event.keystroke, cx) - || self.turn_key(&event.keystroke, cx) + || self.turn_key(&event.keystroke) || self.key_down(&event.keystroke) { cx.stop_propagation(); diff --git a/crates/ely-lok/src/view/lane.rs b/crates/ely-lok/src/view/lane.rs index 6920473..bc9daf8 100644 --- a/crates/ely-lok/src/view/lane.rs +++ b/crates/ely-lok/src/view/lane.rs @@ -9,7 +9,8 @@ use std::{ use futures::{StreamExt, channel::mpsc, select_biased}; use gpui::Context; -use super::{Geom, LokError, LokView, Reported, TILE_TWIPS, input::Input}; +use super::input::{Back, Input}; +use super::{Geom, LokError, LokView, Reported, TILE_TWIPS, Turn}; use crate::{Doc, Tile, Worker}; /// An input slower than this is logged as a warning. @@ -24,7 +25,7 @@ pub(super) struct Job { /// What came back from the lane. enum Landed { /// Sequence, whether it edits, whether it switched part, outcome. - Input(u64, bool, bool, (io::Result>, Option)), + Input(u64, bool, bool, (io::Result>, Option)), Tile(Job, io::Result), } @@ -111,7 +112,8 @@ impl LokView { } self.seen_revision(now.unwrap_or(0)); match outcome { - Ok(Some(copied)) => self.copied = Some(copied), + Ok(Some(Back::Copied(copied))) => self.copied = Some(copied), + Ok(Some(Back::Turned(step))) => cx.emit(Turn(step)), Ok(None) => {} Err(error) => { log::error!("lok: input failed ({:?})", error.kind()); diff --git a/crates/ely-lok/src/view/turn.rs b/crates/ely-lok/src/view/turn.rs index 584b386..74a6ef1 100644 --- a/crates/ely-lok/src/view/turn.rs +++ b/crates/ely-lok/src/view/turn.rs @@ -5,6 +5,7 @@ use std::time::{Duration, Instant}; use gpui::{Context, EventEmitter, Keystroke, Modifiers, ScrollDelta, ScrollWheelEvent, TouchPhase}; use super::LokView; +use super::input::{Input, vcl_key}; /// Asks for the part `step` away from the shown one. #[derive(Clone, Copy, Debug, PartialEq)] @@ -14,53 +15,90 @@ impl EventEmitter for LokView {} /// Trackpad pixels one turn takes. const SWIPE: f32 = 50.0; -/// A pause this long ends a trackpad gesture. +/// A pause this long ends a gesture without phases. const PAUSE: Duration = Duration::from_millis(250); +/// Lines one wheel notch scrolls on this platform. +pub fn notch() -> f32 { + #[cfg(windows)] + { + use windows_sys::Win32::UI::WindowsAndMessaging::{SPI_GETWHEELSCROLLLINES, SystemParametersInfoW}; + let mut lines = 3u32; + // Same setting gpui scales the wheel by. + unsafe { SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, (&raw mut lines).cast(), 0) }; + lines as f32 + } + #[cfg(target_os = "macos")] + { + 1.0 + } + // gpui's X11 and Wayland scroll three lines a notch. + #[cfg(not(any(windows, target_os = "macos")))] + { + 3.0 + } +} + /// The wheel's progress towards a turn. #[derive(Default)] pub(super) struct Turning { - lines: f32, - pixels: f32, + /// This gesture's travel, in turns. + travel: f32, /// This gesture already turned. spent: bool, + /// The platform began a gesture it has not ended. + held: bool, + /// Fractional lines: a smooth touchpad, not a wheel. + smooth: bool, last: Option, } impl Turning { /// The turn a wheel event makes; 0 for none. - pub(super) fn wheel(&mut self, delta: ScrollDelta, phase: TouchPhase, now: Instant) -> i32 { + pub(super) fn wheel(&mut self, delta: ScrollDelta, phase: TouchPhase, now: Instant, notch: f32) -> i32 { let paused = self.last.is_none_or(|last| now.duration_since(last) >= PAUSE); self.last = Some(now); - let ahead = match delta { - // Each notch turns once. + let travel = match delta { + ScrollDelta::Lines(_) if notch <= 0.0 => return 0, ScrollDelta::Lines(lines) => { - if paused { - self.lines = 0.0; + let notches = lines.y / notch; + let whole = notches.round(); + // Whole notches outside a smooth stream: each turns once. + if whole != 0.0 && (notches - whole).abs() < 1e-3 && (paused || !self.smooth) { + (self.travel, self.spent, self.smooth) = (0.0, false, false); + return -(whole as i32); } - self.lines += lines.y; - let whole = self.lines.trunc(); - self.lines -= whole; - whole + if paused || !self.smooth { + (self.travel, self.spent) = (0.0, false); + } + self.smooth = true; + notches } - // One trackpad gesture turns once, momentum included. ScrollDelta::Pixels(pixels) => { - if paused || phase == TouchPhase::Started { - (self.pixels, self.spent) = (0.0, false); + self.smooth = false; + // Without phases, only a pause ends a gesture. + if phase == TouchPhase::Started || paused && !self.held { + (self.travel, self.spent) = (0.0, false); } - if self.spent { - return 0; - } - self.pixels += f32::from(pixels.y); - if self.pixels.abs() < SWIPE { - return 0; - } - self.spent = true; - self.pixels.signum() + self.held = match phase { + TouchPhase::Started => true, + TouchPhase::Ended | TouchPhase::Cancelled => false, + TouchPhase::Moved => self.held, + }; + f32::from(pixels.y) / SWIPE } }; + // One gesture turns once, momentum included. + if self.spent { + return 0; + } + self.travel += travel; + if self.travel.abs() < 1.0 { + return 0; + } + self.spent = true; // Content moving down shows the part before. - -(ahead as i32) + -(self.travel.signum() as i32) } } @@ -75,10 +113,9 @@ impl LokView { self.part } - /// Up and Down turn while nothing is picked or edited. - pub(super) fn turn_key(&mut self, keystroke: &Keystroke, cx: &mut Context) -> bool { - let idle = !self.caret() && self.graphic.is_none() && self.selection.is_empty(); - if self.turning.is_none() || !idle || keystroke.modifiers != Modifiers::none() { + /// Queues Up, Down; the worker turns if nothing is picked. + pub(super) fn turn_key(&mut self, keystroke: &Keystroke) -> bool { + if self.turning.is_none() || keystroke.modifiers != Modifiers::none() { return false; } let step = match keystroke.key.as_str() { @@ -86,7 +123,10 @@ impl LokView { "up" => -1, _ => return false, }; - cx.emit(Turn(step)); + let Some((char_code, key_code)) = vcl_key(keystroke) else { + return false; + }; + self.send(Input::Turn { char_code, key_code, step }); true } @@ -99,7 +139,7 @@ impl LokView { let Some(turning) = self.turning.as_mut().filter(|_| whole) else { return false; }; - let step = turning.wheel(event.delta, event.touch_phase, Instant::now()); + let step = turning.wheel(event.delta, event.touch_phase, Instant::now(), notch()); if step != 0 { cx.emit(Turn(step)); } @@ -113,30 +153,58 @@ mod tests { use super::{PAUSE, Turning}; + const NOTCH: f32 = 3.0; + + fn lines(y: f32) -> ScrollDelta { + ScrollDelta::Lines(point(0.0, y)) + } + + fn pixels(y: f32) -> ScrollDelta { + ScrollDelta::Pixels(point(px(0.0), px(y))) + } + #[test] fn each_notch_turns_once() { let (mut turning, now) = (Turning::default(), std::time::Instant::now()); - let notch = |y| ScrollDelta::Lines(point(0.0, y)); - assert_eq!(turning.wheel(notch(-1.0), TouchPhase::Moved, now), 1); - assert_eq!(turning.wheel(notch(-1.0), TouchPhase::Moved, now), 1); - assert_eq!(turning.wheel(notch(1.0), TouchPhase::Moved, now), -1); - assert_eq!(turning.wheel(notch(0.5), TouchPhase::Moved, now), 0); - assert_eq!(turning.wheel(notch(0.5), TouchPhase::Moved, now), -1); + let mut wheel = |y| turning.wheel(lines(y), TouchPhase::Moved, now, NOTCH); + assert_eq!(wheel(-3.0), 1, "Windows and Linux scroll three lines a notch"); + assert_eq!(wheel(-3.0), 1); + assert_eq!(wheel(3.0), -1); + assert_eq!(wheel(-6.0), 2, "two notches in one event"); + let mut mac = Turning::default(); + assert_eq!(mac.wheel(lines(-1.0), TouchPhase::Moved, now, 1.0), 1); + } + + #[test] + fn smooth_lines_turn_once_a_gesture() { + let (mut turning, now) = (Turning::default(), std::time::Instant::now()); + // An X11 or Windows touchpad: fractions, no phases. + let turned: i32 = (0..30).map(|_| turning.wheel(lines(-0.4), TouchPhase::Moved, now, NOTCH)).sum(); + assert_eq!(turned, 1, "one swipe turns once"); + assert_eq!(turning.wheel(lines(-3.0), TouchPhase::Moved, now, NOTCH), 0, "a whole step mid-swipe"); + let later = now + PAUSE; + assert_eq!(turning.wheel(lines(-3.0), TouchPhase::Moved, later, NOTCH), 1, "a pause ends the swipe"); } #[test] fn a_swipe_turns_once_until_the_next() { let (mut turning, now) = (Turning::default(), std::time::Instant::now()); - let swipe = |y| ScrollDelta::Pixels(point(px(0.0), px(y))); - assert_eq!(turning.wheel(swipe(-30.0), TouchPhase::Started, now), 0); - assert_eq!(turning.wheel(swipe(-30.0), TouchPhase::Moved, now), 1); + let mut swipe = |y, phase, at| turning.wheel(pixels(y), phase, at, NOTCH); + assert_eq!(swipe(-30.0, TouchPhase::Started, now), 0); + assert_eq!(swipe(-30.0, TouchPhase::Moved, now), 1); + // A pause with fingers down is the same gesture. + assert_eq!(swipe(-60.0, TouchPhase::Moved, now + PAUSE * 2), 0); + assert_eq!(swipe(0.0, TouchPhase::Ended, now + PAUSE * 2), 0); // Momentum after the turn turns nothing more. for _ in 0..20 { - assert_eq!(turning.wheel(swipe(-80.0), TouchPhase::Moved, now), 0); + assert_eq!(swipe(-80.0, TouchPhase::Moved, now + PAUSE * 2), 0); } - assert_eq!(turning.wheel(swipe(60.0), TouchPhase::Started, now), -1); - // A pause ends a gesture without phases. - let later = now + PAUSE; - assert_eq!(turning.wheel(swipe(60.0), TouchPhase::Moved, later), -1); + assert_eq!(swipe(60.0, TouchPhase::Started, now + PAUSE * 2), -1); + assert_eq!(swipe(0.0, TouchPhase::Ended, now + PAUSE * 2), 0); + // Without phases a pause ends a gesture (Wayland). + let later = now + PAUSE * 4; + assert_eq!(swipe(60.0, TouchPhase::Moved, later), -1); + assert_eq!(swipe(60.0, TouchPhase::Moved, later), 0); + assert_eq!(swipe(60.0, TouchPhase::Moved, later + PAUSE), -1); } } diff --git a/crates/ely-lok/src/worker.rs b/crates/ely-lok/src/worker.rs index ea5446b..be36349 100644 --- a/crates/ely-lok/src/worker.rs +++ b/crates/ely-lok/src/worker.rs @@ -218,6 +218,7 @@ fn answer(lok: &Lok, tag: u8, id: u64, payload: &[u8]) { } }), tag::POST_KEY => input::key(unpack), + tag::TURN_KEY => input::turn_key(unpack), tag::POST_MOUSE => input::mouse(unpack), tag::GRAPHIC => input::graphic(unpack), tag::POST_TEXT => input::text(unpack), diff --git a/crates/ely-lok/src/worker/input.rs b/crates/ely-lok/src/worker/input.rs index 4c42314..0ca6991 100644 --- a/crates/ely-lok/src/worker/input.rs +++ b/crates/ely-lok/src/worker/input.rs @@ -99,6 +99,20 @@ pub(super) fn key(unpack: &mut Unpack) -> io::Result> { Ok(Vec::new()) } +/// Up or Down after earlier input: 1 if it turns. +pub(super) fn turn_key(unpack: &mut Unpack) -> io::Result> { + let (id, char_code, key_code) = (unpack.u64()?, unpack.i64()?, unpack.i64()?); + // Earlier input's callbacks already landed here. + if !crate::events::picked(id) { + return Ok(Pack::new().u64(1).bytes()); + } + with_doc(id, |doc| { + doc.post_key(0, char_code as i32, key_code as i32); + doc.post_key(1, char_code as i32, key_code as i32); + })?; + Ok(Pack::new().u64(0).bytes()) +} + /// One mouse event: kind, x, y, count, buttons, modifiers. pub(super) fn mouse(unpack: &mut Unpack) -> io::Result> { let id = unpack.u64()?; diff --git a/crates/ely-lok/src/worker/revision.rs b/crates/ely-lok/src/worker/revision.rs index 9888a05..4f0c1c4 100644 --- a/crates/ely-lok/src/worker/revision.rs +++ b/crates/ely-lok/src/worker/revision.rs @@ -51,7 +51,7 @@ pub fn expect(unpack: &mut Unpack) -> io::Result> { pub fn bump(request: u8, payload: &[u8]) { let changes = matches!( request, - tag::POST_KEY | tag::POST_MOUSE | tag::GRAPHIC | tag::POST_TEXT | tag::POST_UNO | tag::PASTE | tag::CUT | tag::COMPLETE_FUNCTION + tag::POST_KEY | tag::TURN_KEY | tag::POST_MOUSE | tag::GRAPHIC | tag::POST_TEXT | tag::POST_UNO | tag::PASTE | tag::CUT | tag::COMPLETE_FUNCTION ); let mut unpack = Unpack::new(payload); let Ok(id) = unpack.u64() else { diff --git a/tasks/TASKS.md b/tasks/TASKS.md index 523f1c7..6e6f8be 100644 --- a/tasks/TASKS.md +++ b/tasks/TASKS.md @@ -271,5 +271,5 @@ - [x] R6.11 Windows 升级后旧文件仍在(R6.10 本机重装查出):安装程序只覆盖不删除,旧版 ElyMark 的 `git` 与 CEF 文件留在安装目录;安装程序记下所装的文件,升级与卸载只移去这些;文件被占用即什么也不改,失败时放回旧版。第 3 轮后(最后一轮,修完不再审查):备份按清单次序编号,近 260 字的路径照样升级与卸载;卸载先查清单本身,删不掉即放回全部 - [x] R6.10 重发 0.1.5(用户定:仍为 0.1.5):删去 v0.1.5 的全部文件,标签移到修复的提交,三端全部包重建并上传,本机重装(标签在 53630ce;推标签即起发布流程,其 Windows 与 Linux 任务建出并上传,与本机同时起的 Windows 构建撞上,本机那份未上传;macOS 任务未上传,dmg 由 Mac mini 本机建、公证、钉票;44 个文件,同名的源码包只留 Linux 的一份,见 P3-16;下载回的 ElyMark 安装程序与清单的散列与构建相同;本机六个应用 0.1.5 经新安装程序升级,ElyMark 目录无 `git`,冒烟启动均正常退出、日志无警告) - [x] R6.12 macOS 打开一个文档,程序坞里 ElyDoc、ElyTable、ElyShow 各出现两个图标(用户 2026-10-15 报):LOK worker 在应用包内启动 AppKit,系统按包的 Info.plist 把它也登记为常规应用(`lsappinfo` 两个 Foreground);worker 在内核启动 AppKit 前把本进程主包信息的 `LSUIElement` 设为真,登记即为辅助应用,钩子后核对策略(第 1 轮:原先登记后才改,15 次有 11 次先为 Foreground);`tests/scripts/dock-macos.sh` 在 Mac mini 上 30 次启动全为 UIElement,旧 worker 即失败;第 2 轮:包外的 worker(`cargo run`、单测)策略为禁止,原核对即退出,改设辅助,测试另启包外的开发构建(40 次全过);第 3 轮通过 -- [ ] R6.13 ElyShow 编辑区的滚轮与上下键不换页(用户报,三端;放映时正常):内核在未选中对象时把上下键当作滚动窗口,Ely 的画布整页适合、无处可滚;整页可见时滚轮换页(每格一页,触控板一次手势一页),未选中对象、未编辑文字时上下键换页,同缩略图条的路径,到首末页即停 +- [ ] R6.13 ElyShow 编辑区的滚轮与上下键不换页(用户报,三端;放映时正常):内核在未选中对象时把上下键当作滚动窗口,Ely 的画布整页适合、无处可滚;整页可见时滚轮换页(每格一页,触控板一次手势一页),未选中对象、未编辑文字时上下键换页,同缩略图条的路径,到首末页即停;第 1 轮:Windows 一格三行原换三页,X11 触控板的小数行一次滑动换多页,按住手指停顿后再滑又换一页,Tab 后紧接的 Down 按旧状态换页;改为按平台每格行数计格、小数行与像素按手势计、有起止的手势只到结束才算完、上下键由 worker 按输入次序判定 - [ ] R6.14 重发 0.1.5(用户定:仍为 0.1.5,全部重建并重传):删去 v0.1.5 的全部文件,标签移到 R6.12、R6.13 的提交,三端全部包重建并上传,本机重装 diff --git a/tasks/reviews/R6.13-r1.md b/tasks/reviews/R6.13-r1.md new file mode 100644 index 0000000..246012c --- /dev/null +++ b/tasks/reviews/R6.13-r1.md @@ -0,0 +1,9 @@ +FAIL + +- crates/ely-lok/src/view/turn.rs:41 — Logical scroll lines are counted as physical wheel notches. Windows multiplies each notch by the system scroll-line setting (`vendor/gpui-pre-windows/src/events.rs:589`); with three lines configured, one downward notch produces `Lines(0, -3)`. Replaying that event through ElyShow and the rebuilt LOK worker moved from slide 1 to slide 4, rather than slide 2 (`.work/R6.13-r1/probe-explicit.log`). The pinned Linux X11 backend also uses three lines per notch. Normalize platform wheel units before counting slide changes, and cover realistic platform deltas; the existing tests only exercise unit notches. + +- crates/ely-lok/src/view/turn.rs:37 — The line-delta branch never applies the one-turn-per-gesture limit, but the pinned GPUI X11 backend also represents smooth touchpad scrolling as `ScrollDelta::Lines` (`gpui-pre-linux-0.3.8`, `x11/client.rs`, `get_scroll_delta_and_update_state` and `make_scroll_wheel_event`). Replaying ten consecutive `-0.4` line deltas in one UI update through ElyShow/LOK moved from slide 1 to slide 5 instead of slide 2 (`.work/R6.13-r1/probe-extra.log`). Preserve the distinction between discrete wheel steps and smooth XInput gestures in the input adapter, and apply gesture limiting to the latter; clamping each event individually would still turn repeatedly during one swipe. + +- crates/ely-lok/src/view/turn.rs:48 — A 250 ms pause resets `spent` even when the platform reports continuation of the same gesture. With a fitted deck, a `Started` event of -60 pixels turns one slide; keeping fingers down, pausing 300 ms, then sending a `Moved` event of -60 pixels turns another. This reproduced through ElyShow/LOK with slide index 2 instead of 1 (`.work/R6.13-r1/probe-explicit.log`). Track explicit gesture lifetime when phases are available; elapsed time must not reopen an explicitly ongoing gesture. Reserve timeout inference for streams without gesture boundaries and test continuation after a pause. + +- crates/ely-lok/src/view/turn.rs:80 — The arrow-key decision uses the last selection callback without accounting for earlier queued input. On a title slide, press Tab to select its placeholder and Down before the worker's selection callback arrives: Down emits `Turn`, so the queued Tab selects the object and the following part change leaves the slide instead of moving that object. The integration probe changed slide index 0 to 1; its control case, waiting for Tab's selection before Down, stayed on slide 0 (`.work/R6.13-r1/probe-extra.log`). Resolve the arrow's action against the selection/editing state after preceding inputs and their callbacks have landed, while preserving input order, and add this queued-input regression.