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:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-15 19:29:10 -04:00
1 parent bb943fb84b
commit fdb8317d24
14 files changed
+312 -19

No files matched your search

+5
View File
@@ -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"))
+48
View File
@@ -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(())
}
+1 -1
View File
@@ -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")]
+6 -12
View File
@@ -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,
+4 -1
View File
@@ -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();
}
}
+10
View File
@@ -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) {
+142
View File
@@ -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);
}
}
+2 -2
View File
@@ -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);
}
}