R6.13 round 1 fix: notches per platform, the worker decides arrows
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c2897aa21a
commit
d427d05b96
15 files changed
+196
-73
No files matched your search
@@ -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)。
|
||||
@@ -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<Deck>) -> 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");
|
||||
}
|
||||
@@ -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<bool> {
|
||||
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)] {
|
||||
|
||||
@@ -362,11 +362,14 @@ pub fn stop_all(docs: &HashMap<u64, Document>) {
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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<i32> {
|
||||
}
|
||||
|
||||
/// Runs one input; gives a copy's mark and the revision.
|
||||
pub(super) fn run(worker: &Mutex<Worker>, doc: &Doc, input: Input) -> (io::Result<Option<String>>, Option<u64>) {
|
||||
pub(super) fn run(worker: &Mutex<Worker>, doc: &Doc, input: Input) -> (io::Result<Option<Back>>, Option<u64>) {
|
||||
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<Worker>, doc: &Doc, input: Input) -> (io::Resul
|
||||
(copied, now)
|
||||
}
|
||||
|
||||
fn apply(worker: &mut Worker, doc: &Doc, input: Input) -> io::Result<Option<String>> {
|
||||
fn apply(worker: &mut Worker, doc: &Doc, input: Input) -> io::Result<Option<Back>> {
|
||||
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<Option<Stri
|
||||
Input::Theme(scheme) => 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<Vec<&str>> =
|
||||
@@ -299,7 +315,7 @@ impl LokView {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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();
|
||||
|
||||
@@ -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<String>>, Option<u64>)),
|
||||
Input(u64, bool, bool, (io::Result<Option<Back>>, Option<u64>)),
|
||||
Tile(Job, io::Result<Tile>),
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
+113
-45
@@ -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<Turn> 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<Instant>,
|
||||
}
|
||||
|
||||
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<Self>) -> 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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -99,6 +99,20 @@ pub(super) fn key(unpack: &mut Unpack) -> io::Result<Vec<u8>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// Up or Down after earlier input: 1 if it turns.
|
||||
pub(super) fn turn_key(unpack: &mut Unpack) -> io::Result<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
let id = unpack.u64()?;
|
||||
|
||||
@@ -51,7 +51,7 @@ pub fn expect(unpack: &mut Unpack) -> io::Result<Vec<u8>> {
|
||||
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 {
|
||||
|
||||
+1
-1
@@ -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 的提交,三端全部包重建并上传,本机重装
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user