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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user