Files
ElyOffice/crates/ely-lok/src/view.rs
T

495 lines
16 KiB
Rust

//! The GPUI tile canvas for one document (LOK-03, LOK-04, LOK-06).
use std::{
collections::{HashMap, HashSet},
io,
sync::{Arc, Mutex},
};
use gpui::{
App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Pixels, Point, RenderImage,
Task, px,
};
use image::{Frame, ImageBuffer, Rgba};
use crate::{Doc, Events, Rect, Worker};
mod borders;
mod clip;
mod clock;
mod formula;
mod handles;
mod ime;
mod input;
mod lane;
mod off;
mod panes;
mod reads;
mod render;
mod zoom;
pub use zoom::{ZOOM_STEPS, Zoomed};
mod scroll;
mod sheet;
pub use input::{CONTEXT, Copy, Cut, Paste, init};
pub use off::read_off;
pub use render::{paper, scheme};
mod turn;
pub use turn::notch;
pub(crate) use crate::{TILE_TWIPS, TWIPS_PER_PIXEL};
/// Document/view conversion under one zoom and DPI (LOK-04).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Geom {
pub zoom: f32,
pub dpi: f32,
}
impl Geom {
/// Twips to logical pixels; GPUI scales to devices.
pub fn px_of(&self, twips: i32) -> f32 {
twips as f32 * self.zoom / TWIPS_PER_PIXEL
}
/// Logical pixels to twips.
pub fn twips_of(&self, pixels: f32) -> i32 {
(pixels * TWIPS_PER_PIXEL / self.zoom).round() as i32
}
/// Texture pixels for one tile edge at this DPI.
fn tile_pixels(&self) -> u32 {
(self.px_of(TILE_TWIPS) * self.dpi).round().max(1.0) as u32
}
}
/// An input request the engine refused; apps show it.
#[derive(Clone, Debug)]
pub struct LokError(pub String);
/// The kernel reported, or queued input landed.
#[derive(Clone, Debug)]
pub struct Reported;
struct TileEntry {
image: Arc<RenderImage>,
/// Shown until its repaint lands, so edits never flash.
stale: bool,
}
/// Paints one LOK document as a cached tile grid (LOK-03).
pub struct LokView {
worker: Arc<Mutex<Worker>>,
doc: Doc,
geom: Geom,
scroll: (i32, i32),
doc_size: (i32, i32),
generation: u64,
tiles: HashMap<(i32, i32), TileEntry>,
/// Tiles no longer drawn, freed at the next paint.
retired: ely_gpui_component::primitives::Retired,
fetching: HashSet<(i32, i32)>,
failed: HashSet<((i32, i32), u64)>,
/// The area last told to the kernel.
shown: Option<(Rect, (u32, i32))>,
viewport: (f32, f32),
origin: Point<Pixels>,
/// Pixels a small document sits in: left, top.
spare: (f32, f32),
/// Frozen columns and rows, in twips from the top left.
frozen: (i32, i32),
/// A spreadsheet's rows and columns, in print twips.
sheet: Option<crate::sheet::Geometry>,
/// The farthest point a jump needed; Calc grows to it.
reach: (i32, i32),
pages: Vec<Rect>,
focus: FocusHandle,
lane: lane::Lane,
sent: u64,
applied: u64,
edited: u64,
moves: u64,
last_edit: u64,
clock: clock::EditClock,
/// A tile reached the screen; logged once (PERF-03).
drawn: bool,
/// Tiles the last frame showed.
on_screen: Vec<(i32, i32)>,
/// The mark of this view's last copy.
copied: Option<String>,
/// The event watch; it ends with the view.
watching: Option<Task<()>>,
cursor: Option<Rect>,
cursor_visible: bool,
pointer: gpui::CursorStyle,
selection: Vec<Rect>,
cell: crate::Cell,
/// Whether the kernel was last told to paint dark.
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,
marked: Option<ime::Marked>,
dragging: bool,
grip: Option<handles::Grip>,
borders: borders::Borders,
handle_size: Pixels,
states: HashMap<String, String>,
graphic: Option<Rect>,
review: u64,
/// The last search outcome: its count and matches.
search: (u64, Vec<Rect>),
menu: Option<String>,
right_at: Option<Point<Pixels>>,
fonts: Vec<String>,
a11y: crate::A11y,
tabs: Vec<i32>,
}
/// A document's size and page frames, in twips.
pub type Layout = ((i64, i64), Vec<Rect>);
use sheet::{Measured, measure};
/// 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)?))
}
impl EventEmitter<LokError> for LokView {}
impl EventEmitter<Reported> for LokView {}
impl LokView {
/// A view of `doc` at `layout`; starts the event watch.
pub fn open(
worker: Arc<Mutex<Worker>>,
doc: Doc,
(size, pages): Layout,
cx: &mut App,
) -> Entity<Self> {
cx.new(|cx| Self::sized(worker, doc, size, pages, cx))
}
fn sized(
worker: Arc<Mutex<Worker>>,
doc: Doc,
(width, height): (i64, i64),
pages: Vec<Rect>,
cx: &mut Context<Self>,
) -> Self {
log::info!("lok: {} pages", pages.len());
let lane = Self::lane(Arc::clone(&worker), doc.clone(), cx);
let mut view = Self {
worker,
doc,
geom: Geom { zoom: 1.0, dpi: 1.0 },
scroll: (0, 0),
doc_size: (width as i32, height as i32),
generation: 0,
tiles: HashMap::new(),
retired: Default::default(),
fetching: HashSet::new(),
failed: HashSet::new(),
shown: None,
viewport: (0.0, 0.0),
origin: Point::default(),
spare: (0.0, 0.0),
frozen: (0, 0),
sheet: None,
reach: (0, 0),
pages,
focus: cx.focus_handle().tab_stop(true),
lane,
sent: 0,
applied: 0,
edited: 0,
moves: 0,
last_edit: 0,
clock: clock::EditClock::default(),
drawn: false,
on_screen: Vec::new(),
copied: None,
cursor: None,
cursor_visible: true,
pointer: gpui::CursorStyle::Arrow,
selection: Vec::new(),
cell: crate::Cell::default(),
dark: None,
fit: false,
turning: None,
part: 0,
picked: 0,
marked: None,
dragging: false,
grip: None,
borders: Default::default(),
handle_size: px(6.0),
states: HashMap::new(),
graphic: None,
review: 0,
search: (0, Vec::new()),
menu: None,
right_at: None,
fonts: Vec::new(),
a11y: crate::A11y::default(),
tabs: Vec::new(),
watching: None,
};
view.watching = Some(view.watch(cx));
view
}
/// Repaints every tile; the kernel sent no notice.
pub fn invalidate(&mut self, cx: &mut Context<Self>) {
for tile in self.tiles.values_mut() {
tile.stale = true;
}
self.failed.clear();
cx.notify();
}
/// Zooms so one page fits the viewport, from now on.
pub fn fit_page(&mut self, cx: &mut Context<Self>) {
if !self.fit {
self.fit = true;
cx.emit(Zoomed);
cx.notify();
}
}
/// The tiles painted for the current state (test evidence).
#[cfg(test)]
pub fn tile_count(&self) -> usize {
self.tiles.values().filter(|tile| !tile.stale).count()
}
/// Runs a table command; arguments are checked now (LOK-02).
pub fn run(&mut self, id: &str, values: &[&str]) -> io::Result<()> {
let command = crate::commands::find(id)?;
if command.addressed(values) {
return Err(io::Error::other(format!("{id} names something by number; run it at the state it was read")));
}
self.send(input::Input::Run { commands: vec![command.owned(values)?], at: None });
Ok(())
}
/// The document as the view last saw it.
pub fn read_at(&self) -> Doc {
self.doc.clone()
}
/// Batches and landings arrive out of order; keep the latest.
fn seen_revision(&mut self, revision: u64) {
self.doc.rev = self.doc.rev.max(revision);
}
/// Runs `id` against `at`, the document as it was read.
pub fn run_at(&mut self, at: &crate::Doc, id: &str, values: &[&str]) -> io::Result<()> {
self.run_all_at(at, &[(id, values)])
}
/// Runs `commands` in order, all made from the read `at`.
pub fn run_all_at(&mut self, at: &crate::Doc, commands: &[(&str, &[&str])]) -> io::Result<()> {
let commands = commands.iter().map(|(id, values)| crate::commands::find(id)?.owned(values)).collect::<io::Result<_>>()?;
self.send(input::Input::Run { commands, at: Some(at.clone()) });
Ok(())
}
/// Takes pushed events; the worker lock stays free (LOK-03).
fn watch(&self, cx: &mut Context<Self>) -> Task<()> {
let feed = self.worker.lock().expect("the worker lock").feed();
let (worker, doc) = (Arc::clone(&self.worker), self.doc.clone());
cx.spawn(async move |this, cx| {
// The resize count measured; none yet.
let (mut known, mut resized) = (0u64, None);
loop {
if this.upgrade().is_none() {
break;
}
// Awaited, not blocked: no thread waits for events.
let batch = match feed.next(&doc, known).await {
Ok(batch) => {
let measured = if resized != Some(batch.resized) {
let (worker, doc) = (Arc::clone(&worker), doc.clone());
let read = async move { measure(&worker, &doc) };
Some(cx.background_executor().spawn(read).await)
} else {
None
};
Ok((batch, measured))
}
Err(error) => Err(error),
};
let batch = match batch {
Ok(batch) => batch,
Err(error) => {
log::error!("lok: event watch ends: {error}");
let message = format!("the document engine stopped: {error}");
if this.update(cx, |_, cx| cx.emit(LokError(message))).is_err() {
log::info!("lok: the view went first");
}
break;
}
};
let (batch, measured) = batch;
(known, resized) = (batch.seq, Some(batch.resized));
if this
.update(cx, |view, cx| view.apply_batch(batch, measured, cx))
.is_err()
{
break;
}
}
})
}
/// Takes the cursor; drops the tiles a batch dirties (LOK-03).
fn apply_batch(&mut self, batch: Events, measured: Option<Measured>, cx: &mut Context<Self>) {
// What the view shows is the document at this revision.
self.seen_revision(batch.revision);
match measured {
Some(Ok((((width, height), pages), zoom))) => {
self.doc_size = self.at_scale((width as i32, height as i32), zoom);
if pages.len() != self.pages.len() {
log::info!("lok: {} pages", pages.len());
}
self.pages = pages;
self.clamp_scroll();
}
Some(Err(error)) => log::warn!("lok: document size lost: {error}"),
None => {}
}
if batch.cursor != self.cursor {
self.moves += 1;
// A moved cursor stays in view, but not mid-drag.
if self.viewport.1 > 0.0
&& !self.dragging
&& let Some(cursor) = batch.cursor
{
self.show(cursor, cx);
}
}
// A moved cell cursor stays in view too.
if batch.cell.cursor != self.cell.cursor
&& self.viewport.1 > 0.0
&& !self.dragging
&& let Some(cell) = batch.cell.cursor
{
self.show(cell, cx);
}
self.follow_graphic(batch.graphic, cx);
self.cursor = batch.cursor;
self.cursor_visible = batch.cursor_visible;
if !batch.pointer.is_empty() {
self.pointer = render::pointer(&batch.pointer);
}
self.selection = batch.selection;
if batch.cell.functions != self.cell.functions {
self.picked = 0;
}
self.cell = batch.cell;
self.graphic = batch.graphic;
self.read_borders(batch.table, batch.revision);
self.review = batch.review;
self.search = (batch.searched, batch.found);
self.fonts = batch.fonts;
if batch.menu.is_some() {
self.menu = batch.menu;
}
for fault in batch.faults {
cx.emit(LokError(fault));
}
self.a11y = batch.a11y;
self.tabs = batch.tabs;
self.states.extend(batch.states);
for title in batch.blocked {
cx.emit(LokError(format!(
"the kernel opened a dialog Ely closed: {title}"
)));
}
cx.emit(Reported);
if batch.generation == self.generation && !batch.all && batch.rects.is_empty() {
cx.notify();
return;
}
self.generation = batch.generation;
self.clock.bumped(batch.revision, batch.generation);
let rects = &batch.rects;
for (&(kx, ky), tile) in &mut self.tiles {
let (tx, ty) = (kx * TILE_TWIPS, ky * TILE_TWIPS);
tile.stale |= batch.all
|| rects.iter().any(|&(x, y, w, h)| {
tx < x + w && x < tx + TILE_TWIPS && ty < y + h && y < ty + TILE_TWIPS
});
}
// Shown tiles repaint now, not a frame later.
let dirty: Vec<_> = self
.on_screen
.iter()
.copied()
.filter(|key| self.tiles.get(key).is_some_and(|tile| tile.stale))
.collect();
for key in dirty {
self.request_tile(key);
}
cx.notify();
}
/// Requests one missing tile on the background executor.
fn request_tile(&mut self, key: (i32, i32)) {
if self.failed.contains(&(key, self.generation)) || !self.fetching.insert(key) {
return;
}
let job = lane::Job {
key,
geom: self.geom,
};
self.lane
.tiles
.unbounded_send(job)
.expect("the lane lives with the view");
}
/// Takes a painted tile; stale generations never land (LOK-03).
fn finish_tile(
&mut self,
key: (i32, i32),
geom: Geom,
tile: io::Result<crate::Tile>,
cx: &mut Context<Self>,
) {
self.fetching.remove(&key);
match tile {
Ok(tile) if tile.generation >= self.generation && geom == self.geom => {
let side = geom.tile_pixels();
// LOK and RenderImage both take BGRA bytes.
let Some(image) = ImageBuffer::<Rgba<u8>, _>::from_raw(side, side, tile.pixels)
else {
log::warn!("lok: a tile came back the wrong size");
return;
};
let image = Arc::new(RenderImage::new([Frame::new(image)]));
self.clock.painted(tile.generation, Arc::as_ptr(&image) as usize);
if let Some(old) = self.tiles.insert(key, TileEntry { image, stale: false }) {
self.retired.push(old.image);
}
cx.notify();
}
Ok(_) => cx.notify(),
Err(error) => {
// Retried at the next change or move.
log::warn!("lok: tile {key:?} failed: {error}");
self.failed.insert((key, self.generation));
cx.emit(LokError(format!("a page failed to paint: {error}")));
}
}
}
}
#[cfg(test)]
mod tests;