R6.12, R6.13: the LOK worker has no Dock icon; Show turns slides
R6.12: on macOS the worker sets LibreOffice's own NSApplication to the accessory policy before the kit starts, so it never shows in the Dock. R6.13: when the whole slide fits, the wheel and Up/Down (nothing selected, no text editing) turn slides in the ElyShow editor. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bb943fb84b
commit
fdb8317d24
14 files changed
+312
-19
No files matched your search
@@ -303,3 +303,5 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
|
||||
- 2026-10-15(用户报:ElyMark 代码块后的“你好↵测试”离开输入框后成了“你好 测试”):单个换行照 Typora 的默认(编辑时保留,导出与打印不保留):组件库 93efa7f 起 `MarkdownReader::new`(屏上:可视、预览、右键与命令的读法)把软换行读作换行,`MarkdownReader::joined`(输出)照 GFM 读作空格;标题的大纲与目录文字照旧一行。ElyMark 的 HTML、PDF、富文本复制与纯文本导出用 `joined`(R6.7)。组件库 c31d092 与 26af4be:画出成一个换行的 CRLF 对回其起点(原先按在 CRLF 行末键入落在 CR 与 LF 之间),行内代码照留两字则逐字对回。
|
||||
- 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 在内核启动前(`Lok::hook` 打开内核之后、调钩子之前)载入同一目录的 Aqua 插件 `libvclplug_osxlo.dylib`,取内核自己的 `VCL_NSApplication`,设为辅助应用(`NSApplicationActivationPolicyAccessory`:无程序坞图标、无菜单栏,仍可建屏外窗口),内核随后复用同一实例;类不在或设不上即报错,不启动(`ffi/accessory.rs`)。实测(Mac mini):worker 从未是 Foreground,登记即为 UIElement,图块照出(R6.12)。
|
||||
- 2026-10-15(用户报:ElyShow 编辑区滚轮与上下键不换页,三端):内核在未选中对象时把上下键当作滚动窗口(`fupoor.cxx`),Ely 的画布整页适合、无处可滚,滚轮同样。`LokView::turn_parts`(ElyShow 首次适合时调用)之后:整页可见时滚轮换页(`view/turn.rs`:每格一页;触控板一次手势只换一页,惯性不算,静止 250 毫秒或新手势再计),未选中对象、未编辑文字时上下键换页;视图发 `Turn`,ElyShow 从视图所示的页起算(连续换页累加),经缩略图条同一路径 `show_part`,到首末页即停;后台读回的当前页只在视图已无待落地输入时采用,排队的换页不被先前的读取拉回(R6.13)。
|
||||
@@ -95,7 +95,11 @@ impl Host for Deck {
|
||||
&& !tab.extra.fitted
|
||||
{
|
||||
tab.extra.fitted = true;
|
||||
view.update(cx, |view, cx| view.fit_page(cx));
|
||||
view.update(cx, |view, cx| {
|
||||
view.fit_page(cx);
|
||||
view.turn_parts();
|
||||
});
|
||||
cx.subscribe(view, move |deck, view, turn: &ely_lok::Turn, cx| deck.turn(id, &view, turn.0, cx)).detach();
|
||||
}
|
||||
// Pictures follow the kernel's light or dark scheme.
|
||||
let dark = view.read(cx).dark();
|
||||
@@ -263,7 +267,12 @@ impl Deck {
|
||||
Ok(((slides, at), (design, objects, notes, remarks, (motion, media)), size, history)) => {
|
||||
tab.extra.read_at = Some(at);
|
||||
log::info!("show: tab {id} has {} parts", slides.list.len());
|
||||
view.update(cx, |view, _| view.set_part(slides.current));
|
||||
// A queued turn outranks a read made before it.
|
||||
view.update(cx, |view, _| {
|
||||
if view.settled() {
|
||||
view.set_part(slides.current)
|
||||
}
|
||||
});
|
||||
let names: HashSet<&String> =
|
||||
slides.list.iter().map(|slide| &slide.name).collect();
|
||||
tab.extra.keep_pictures(&names);
|
||||
|
||||
@@ -9,10 +9,11 @@ use ely_gpui_component::{
|
||||
};
|
||||
use ely_office::prelude::*;
|
||||
use gpui::{
|
||||
Action, App, Context, Div, InteractiveElement as _, IntoElement, Menu, MenuItem,
|
||||
Action, App, Context, Div, Entity, InteractiveElement as _, IntoElement, Menu, MenuItem,
|
||||
ParentElement as _, Render, RenderImage, Role, StatefulInteractiveElement as _, Styled as _,
|
||||
WeakEntity, Window, actions, div, img, prelude::FluentBuilder as _, px,
|
||||
};
|
||||
use ely_lok::LokView;
|
||||
use image::{Frame, ImageBuffer, Rgba};
|
||||
|
||||
use crate::deck::Deck;
|
||||
@@ -98,6 +99,19 @@ impl Deck {
|
||||
self.run_active(id, &[], cx);
|
||||
}
|
||||
|
||||
/// Shows the slide `step` away, as the strip would.
|
||||
pub(crate) fn turn(&mut self, id: u64, view: &Entity<LokView>, step: i32, cx: &mut Context<Self>) {
|
||||
let Some(count) = self.tab(id).map(|tab| tab.extra.slides.list.len()).filter(|count| *count > 0) else {
|
||||
return;
|
||||
};
|
||||
// From the part asked last, so quick turns add up.
|
||||
let shown = view.read(cx).part();
|
||||
let part = (shown as i64 + i64::from(step)).clamp(0, count as i64 - 1) as usize;
|
||||
if part != shown {
|
||||
view.update(cx, |view, _| view.show_part(part));
|
||||
}
|
||||
}
|
||||
|
||||
/// Paints the next missing or stale slide picture.
|
||||
pub fn paint_next(&mut self, id: u64, cx: &mut Context<Self>) {
|
||||
let Some(tab) = self.tab(id) else {
|
||||
|
||||
@@ -19,6 +19,7 @@ mod play_time;
|
||||
mod review;
|
||||
mod slides;
|
||||
mod strip;
|
||||
mod turn;
|
||||
|
||||
/// An unrendered Deck over a fresh worker, if LOK exists.
|
||||
pub(super) fn deck(cx: &mut TestAppContext, dir: &Path) -> Option<Entity<Deck>> {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use ely_lok::LokView;
|
||||
use ely_office::prelude::*;
|
||||
use gpui::{Entity, ScrollDelta, ScrollWheelEvent, TestAppContext, VisualTestContext, point};
|
||||
|
||||
use super::objects::blank;
|
||||
use super::{counts, quiet, settle, update};
|
||||
use crate::deck::Deck;
|
||||
|
||||
/// The slide canvas alone in a window.
|
||||
struct Host(Entity<LokView>);
|
||||
|
||||
impl gpui::Render for Host {
|
||||
fn render(&mut self, _: &mut gpui::Window, _: &mut gpui::Context<Self>) -> impl gpui::IntoElement {
|
||||
use gpui::{ParentElement as _, Styled as _};
|
||||
gpui::div().size_full().child(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// The current slide once every turn landed.
|
||||
fn current(shown: &mut VisualTestContext, deck: &Entity<Deck>) -> usize {
|
||||
quiet(shown, deck);
|
||||
shown.update(|_, app| deck.read(app).active_tab().map(|tab| tab.extra.slides.current).expect("a tab"))
|
||||
}
|
||||
|
||||
fn wheel(shown: &mut VisualTestContext, lines: f32) {
|
||||
let middle = shown.update(|window, _| window.viewport_size()).center();
|
||||
let delta = ScrollDelta::Lines(point(0.0, lines));
|
||||
shown.simulate_event(ScrollWheelEvent { position: middle.into(), delta, ..Default::default() });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_wheel_and_up_down_turn_slides() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let Some(deck) = blank(&mut cx, dir.path()) else {
|
||||
return eprintln!("SKIP: no LOK build");
|
||||
};
|
||||
for n in 2..=3 {
|
||||
update(&mut cx, &deck, |deck, cx| deck.object_command("slide.insert", &[], cx));
|
||||
settle(&cx, |app| counts(app, &deck).0 == n);
|
||||
}
|
||||
let view = cx.update(|app| deck.read(app).active_tab().expect("a tab").view.clone());
|
||||
cx.update(|app| view.update(app, |view, _| view.show_part(0)));
|
||||
let window = cx.add_window(|_, _| Host(view.clone()));
|
||||
let mut shown = VisualTestContext::from_window(window.into(), &cx);
|
||||
shown.run_until_parked();
|
||||
assert_eq!(current(&mut shown, &deck), 0);
|
||||
wheel(&mut shown, -1.0);
|
||||
assert_eq!(current(&mut shown, &deck), 1, "the wheel turns down");
|
||||
shown.update(|window, app| {
|
||||
let focus = view.read(app).focus_handle().clone();
|
||||
window.focus(&focus, app)
|
||||
});
|
||||
shown.simulate_keystrokes("down");
|
||||
assert_eq!(current(&mut shown, &deck), 2, "Down turns");
|
||||
shown.simulate_keystrokes("down");
|
||||
assert_eq!(current(&mut shown, &deck), 2, "the last slide stays");
|
||||
shown.simulate_keystrokes("up");
|
||||
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");
|
||||
}
|
||||
@@ -60,6 +60,8 @@ macro_rules! call {
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod accessory;
|
||||
mod document;
|
||||
mod slideshow;
|
||||
pub use document::{Raw, close_window};
|
||||
@@ -80,6 +82,9 @@ impl Lok {
|
||||
let install_c = cstring(&install_str)?;
|
||||
let profile_c = cstring(profile_url)?;
|
||||
let lib = Library::open(install)?;
|
||||
// Before the kit starts, so no Dock icon shows.
|
||||
#[cfg(target_os = "macos")]
|
||||
accessory::accessory(install)?;
|
||||
let hook: Hook2 = unsafe {
|
||||
lib.symbol(b"libreofficekit_hook_2\0")
|
||||
.or_else(|_| lib.symbol(b"libreofficekit_hook\0"))
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//! The worker stays out of the Dock (macOS).
|
||||
|
||||
use std::ffi::{CString, c_char, c_void};
|
||||
use std::io;
|
||||
use std::os::unix::ffi::OsStrExt as _;
|
||||
use std::path::Path;
|
||||
|
||||
#[link(name = "objc")]
|
||||
unsafe extern "C" {
|
||||
fn objc_getClass(name: *const c_char) -> *mut c_void;
|
||||
fn sel_registerName(name: *const c_char) -> *mut c_void;
|
||||
fn objc_msgSend();
|
||||
}
|
||||
|
||||
/// The kernel's Aqua plugin; it defines `VCL_NSApplication`.
|
||||
const PLUGIN: &str = "libvclplug_osxlo.dylib";
|
||||
/// NSApplicationActivationPolicyAccessory: no Dock icon, no menu bar.
|
||||
const ACCESSORY: isize = 1;
|
||||
|
||||
/// Makes the kernel's application an accessory before the kit starts.
|
||||
pub fn accessory(install: &Path) -> io::Result<()> {
|
||||
// Loaded for good; the kernel later opens the same image.
|
||||
let path = install.join(PLUGIN);
|
||||
let c = CString::new(path.as_os_str().as_bytes()).map_err(|_| io::Error::other("the path holds a NUL"))?;
|
||||
if unsafe { libc::dlopen(c.as_ptr(), libc::RTLD_LAZY | libc::RTLD_GLOBAL) }.is_null() {
|
||||
return Err(io::Error::other(format!("dlopen {}: {}", path.display(), super::dl_error())));
|
||||
}
|
||||
// The kernel's own class, so it reuses this instance.
|
||||
let class = unsafe { objc_getClass(c"VCL_NSApplication".as_ptr()) };
|
||||
if class.is_null() {
|
||||
return Err(io::Error::other(format!("{PLUGIN} has no VCL_NSApplication class")));
|
||||
}
|
||||
type Shared = unsafe extern "C" fn(*mut c_void, *mut c_void) -> *mut c_void;
|
||||
type Policy = unsafe extern "C" fn(*mut c_void, *mut c_void, isize) -> i8;
|
||||
let send = objc_msgSend as unsafe extern "C" fn();
|
||||
let shared: Shared = unsafe { std::mem::transmute(send) };
|
||||
let policy: Policy = unsafe { std::mem::transmute(send) };
|
||||
let app = unsafe { shared(class, sel_registerName(c"sharedApplication".as_ptr())) };
|
||||
if app.is_null() {
|
||||
return Err(io::Error::other("the kernel's application did not start"));
|
||||
}
|
||||
let set = unsafe { policy(app, sel_registerName(c"setActivationPolicy:".as_ptr()), ACCESSORY) };
|
||||
if set == 0 {
|
||||
return Err(io::Error::other("macOS refused the accessory policy"));
|
||||
}
|
||||
log::info!("lok: the worker is an accessory app");
|
||||
Ok(())
|
||||
}
|
||||
@@ -20,7 +20,7 @@ pub use client::{
|
||||
};
|
||||
#[cfg(feature = "view")]
|
||||
pub use view::{
|
||||
CONTEXT, Copy, Cut, Geom, Layout, LokError, LokView, Paste, Reported, ZOOM_STEPS,
|
||||
CONTEXT, Copy, Cut, Geom, Layout, LokError, LokView, Paste, Reported, Turn, ZOOM_STEPS,
|
||||
Zoomed, init, layout, paper, read_off, scheme,
|
||||
};
|
||||
#[cfg(feature = "view")]
|
||||
|
||||
@@ -32,7 +32,9 @@ mod scroll;
|
||||
mod sheet;
|
||||
pub use input::{CONTEXT, Copy, Cut, Paste, init};
|
||||
pub use off::read_off;
|
||||
pub use render::scheme;
|
||||
pub use render::{paper, scheme};
|
||||
mod turn;
|
||||
pub use turn::Turn;
|
||||
|
||||
pub(crate) use crate::{TILE_TWIPS, TWIPS_PER_PIXEL};
|
||||
|
||||
@@ -125,6 +127,8 @@ pub struct LokView {
|
||||
dark: Option<bool>,
|
||||
/// The zoom follows the viewport so a page fits.
|
||||
fit: bool,
|
||||
/// Set when the wheel and Up, Down turn parts.
|
||||
turning: Option<turn::Turning>,
|
||||
/// The sheet shown, and the offered function chosen.
|
||||
part: usize,
|
||||
picked: usize,
|
||||
@@ -150,17 +154,6 @@ pub type Layout = ((i64, i64), Vec<Rect>);
|
||||
|
||||
use sheet::{Measured, measure};
|
||||
|
||||
/// A page's colour; the kernel's Ely schemes share it.
|
||||
pub fn paper(cx: &gpui::App) -> gpui::Hsla {
|
||||
use ely_gpui_component::theme::ActiveTheme as _;
|
||||
let theme = cx.theme();
|
||||
if theme.is_dark() {
|
||||
theme.colors.surface
|
||||
} else {
|
||||
theme.colors.paper
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads `doc`'s size and page frames.
|
||||
pub fn layout(worker: &mut Worker, doc: &Doc) -> io::Result<Layout> {
|
||||
Ok((doc.size(worker)?, doc.pages(worker)?))
|
||||
@@ -227,6 +220,7 @@ impl LokView {
|
||||
cell: crate::Cell::default(),
|
||||
dark: None,
|
||||
fit: false,
|
||||
turning: None,
|
||||
part: 0,
|
||||
picked: 0,
|
||||
marked: None,
|
||||
|
||||
@@ -298,7 +298,10 @@ impl LokView {
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.walk(&event.keystroke, cx) || self.key_down(&event.keystroke) {
|
||||
if self.walk(&event.keystroke, cx)
|
||||
|| self.turn_key(&event.keystroke, cx)
|
||||
|| self.key_down(&event.keystroke)
|
||||
{
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,16 @@ pub fn scheme(dark: bool) -> &'static str {
|
||||
if dark { DARK } else { LIGHT }
|
||||
}
|
||||
|
||||
/// A page's colour; the kernel's Ely schemes share it.
|
||||
pub fn paper(cx: &gpui::App) -> gpui::Hsla {
|
||||
let theme = cx.theme();
|
||||
if theme.is_dark() {
|
||||
theme.colors.surface
|
||||
} else {
|
||||
theme.colors.paper
|
||||
}
|
||||
}
|
||||
|
||||
impl LokView {
|
||||
/// Paints with the app's light or dark scheme.
|
||||
pub(super) fn follow_theme(&mut self, dark: bool) {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
//! The wheel and Up, Down turn slides (SHOW-01).
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{Context, EventEmitter, Keystroke, Modifiers, ScrollDelta, ScrollWheelEvent, TouchPhase};
|
||||
|
||||
use super::LokView;
|
||||
|
||||
/// Asks for the part `step` away from the shown one.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Turn(pub i32);
|
||||
|
||||
impl EventEmitter<Turn> for LokView {}
|
||||
|
||||
/// Trackpad pixels one turn takes.
|
||||
const SWIPE: f32 = 50.0;
|
||||
/// A pause this long ends a trackpad gesture.
|
||||
const PAUSE: Duration = Duration::from_millis(250);
|
||||
|
||||
/// The wheel's progress towards a turn.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Turning {
|
||||
lines: f32,
|
||||
pixels: f32,
|
||||
/// This gesture already turned.
|
||||
spent: 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 {
|
||||
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.
|
||||
ScrollDelta::Lines(lines) => {
|
||||
if paused {
|
||||
self.lines = 0.0;
|
||||
}
|
||||
self.lines += lines.y;
|
||||
let whole = self.lines.trunc();
|
||||
self.lines -= whole;
|
||||
whole
|
||||
}
|
||||
// One trackpad gesture turns once, momentum included.
|
||||
ScrollDelta::Pixels(pixels) => {
|
||||
if paused || phase == TouchPhase::Started {
|
||||
(self.pixels, 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()
|
||||
}
|
||||
};
|
||||
// Content moving down shows the part before.
|
||||
-(ahead as i32)
|
||||
}
|
||||
}
|
||||
|
||||
impl LokView {
|
||||
/// The wheel and Up, Down turn parts from now on.
|
||||
pub fn turn_parts(&mut self) {
|
||||
self.turning.get_or_insert_default();
|
||||
}
|
||||
|
||||
/// The part shown.
|
||||
pub fn part(&self) -> usize {
|
||||
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() {
|
||||
return false;
|
||||
}
|
||||
let step = match keystroke.key.as_str() {
|
||||
"down" => 1,
|
||||
"up" => -1,
|
||||
_ => return false,
|
||||
};
|
||||
cx.emit(Turn(step));
|
||||
true
|
||||
}
|
||||
|
||||
/// The wheel turns while the whole page shows.
|
||||
pub(super) fn turn_wheel(&mut self, event: &ScrollWheelEvent, cx: &mut Context<Self>) -> bool {
|
||||
let (width, height) = self.extent();
|
||||
// A pixel of slack for rounding.
|
||||
let whole = self.geom.px_of(width) <= self.viewport.0 + 1.0
|
||||
&& self.geom.px_of(height) <= self.viewport.1 + 1.0;
|
||||
let Some(turning) = self.turning.as_mut().filter(|_| whole) else {
|
||||
return false;
|
||||
};
|
||||
let step = turning.wheel(event.delta, event.touch_phase, Instant::now());
|
||||
if step != 0 {
|
||||
cx.emit(Turn(step));
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{ScrollDelta, TouchPhase, point, px};
|
||||
|
||||
use super::{PAUSE, Turning};
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[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);
|
||||
// Momentum after the turn turns nothing more.
|
||||
for _ in 0..20 {
|
||||
assert_eq!(turning.wheel(swipe(-80.0), TouchPhase::Moved, now), 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);
|
||||
}
|
||||
}
|
||||
@@ -65,9 +65,9 @@ impl LokView {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Ctrl (macOS: Cmd) and the wheel zoom; else scroll.
|
||||
/// Ctrl (macOS: Cmd) wheel zooms; else it turns or scrolls.
|
||||
pub(super) fn on_wheel(&mut self, event: &ScrollWheelEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.wheel_zoom(event, cx) {
|
||||
if !self.wheel_zoom(event, cx) && !self.turn_wheel(event, cx) {
|
||||
self.on_scroll(event, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,3 +270,6 @@
|
||||
- [x] R6.9 ElyMark 三、四级标题不显眼(用户报):各级字号随正文按 Typora 的倍数(2.25、1.75、1.5、1.25、1、1),一律加粗,六级为次要色(组件库渲染)
|
||||
- [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`,冒烟启动均正常退出、日志无警告)
|
||||
- [ ] R6.12 macOS 打开一个文档,程序坞里 ElyDoc、ElyTable、ElyShow 各出现两个图标(用户 2026-10-15 报):LOK worker 在应用包内启动 AppKit,系统按包的 Info.plist 把它也登记为常规应用(`lsappinfo` 两个 Foreground);worker 在内核启动前载入内核的 Aqua 插件,把内核自己的 `VCL_NSApplication` 设为辅助应用(无程序坞图标、无菜单栏,仍可建屏外窗口)
|
||||
- [ ] R6.13 ElyShow 编辑区的滚轮与上下键不换页(用户报,三端;放映时正常):内核在未选中对象时把上下键当作滚动窗口,Ely 的画布整页适合、无处可滚;整页可见时滚轮换页(每格一页,触控板一次手势一页),未选中对象、未编辑文字时上下键换页,同缩略图条的路径,到首末页即停
|
||||
- [ ] R6.14 重发 0.1.5(用户定:仍为 0.1.5,全部重建并重传):删去 v0.1.5 的全部文件,标签移到 R6.12、R6.13 的提交,三端全部包重建并上传,本机重装
|
||||
Reference in new issue
Block a user