//! LokView's element tree and paint (LOK-03, ยง7 tail). use ely_gpui_component::theme::ActiveTheme; use gpui::{ BorderStyle, Bounds, ContentMask, Context, Corners, CursorStyle, Edges, ElementInputHandler, InteractiveElement, IntoElement, MouseButton, ParentElement, Render, Role, StatefulInteractiveElement, Styled, Window, canvas, div, fill, outline, px, quad, size, transparent_black, }; use std::sync::Arc; use gpui::{Pixels, RenderImage, point}; use super::{CONTEXT, LokView, TILE_TWIPS, TWIPS_PER_PIXEL, handles, input}; use crate::Rect; /// Ely's kernel color schemes, from `ely.xcd`. const DARK: &str = "COLOR_SCHEME_ELY_DARK"; const LIGHT: &str = "COLOR_SCHEME_ELY_LIGHT"; /// The kernel scheme for a light or dark app. pub fn scheme(dark: bool) -> &'static str { if dark { DARK } else { LIGHT } } impl LokView { /// Paints with the app's light or dark scheme. pub(super) fn follow_theme(&mut self, dark: bool) { if self.dark != Some(dark) { self.dark = Some(dark); let scheme = scheme(dark); log::info!("lok: paints with {scheme}"); self.send(input::Input::Theme(scheme)); } } } impl Render for LokView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let dpi = window.scale_factor(); if dpi != self.geom.dpi { self.keep_scroll(|view| view.geom.dpi = dpi); self.retire_tiles(); } self.follow_theme(cx.theme().is_dark()); let this = cx.entity().clone(); let focus = self.focus.clone(); let colors = &cx.theme().colors; let paper = super::paper(cx); let (selection_color, caret_color) = (colors.selection, colors.focus); let canvas_color = colors.sunken; let caret_width = cx.theme().caret_width().to_pixels(window.rem_size()); self.handle_size = cx.theme().handle_hit().to_pixels(window.rem_size()); let (frame_color, handle_fill) = (colors.focus, colors.bg); div() .id("lok-view") .role(Role::MultilineTextInput) .aria_value(self.a11y.text.clone()) .key_context(CONTEXT) .track_focus(&self.focus) .cursor(CursorStyle::IBeam) .size_full() .overflow_hidden() .bg(canvas_color) .on_action(cx.listener(Self::on_copy)) .on_action(cx.listener(Self::on_cut)) .on_action(cx.listener(Self::on_next_field)) .on_action(cx.listener(Self::on_previous_field)) .on_action(cx.listener(Self::on_paste)) .on_key_down(cx.listener(Self::on_key_down)) .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) .on_mouse_down(MouseButton::Right, cx.listener(Self::on_mouse_down)) .on_mouse_down(MouseButton::Middle, cx.listener(Self::on_mouse_down)) .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) .on_mouse_up(MouseButton::Right, cx.listener(Self::on_mouse_up)) .on_mouse_up(MouseButton::Middle, cx.listener(Self::on_mouse_up)) .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up_out)) .on_mouse_move(cx.listener(Self::on_mouse_move)) .on_scroll_wheel(cx.listener(Self::on_wheel)) .on_pinch(cx.listener(Self::on_pinch)) .child( canvas( { let this = this.clone(); move |bounds, window, app| { this.update(app, |view, _| { view.retired.release(window); view.prepare(bounds) }) } }, move |bounds, painted, window, app| { let mut drawn = false; for page in &painted.pages { window.paint_quad(fill(*page, paper)); window.with_content_mask( Some(ContentMask { bounds: *page }), |window| { for (rect, image, clip) in &painted.tiles { let mask = Some(ContentMask { bounds: *clip }); let painted = window.with_content_mask(mask, |window| { window.paint_image( *rect, *rect, Corners::default(), image.clone(), 0, false, ) }); match painted { Ok(()) => { drawn = true; let tile = Arc::as_ptr(image) as usize; if this.read(app).clock.awaits(tile) { this.update(app, |view, _| view.clock.drawn(tile)); } } Err(error) => log::error!("lok: tile paint failed: {error}"), } } }, ); } if drawn && !this.read(app).drawn { this.update(app, |view, _| view.drawn = true); log::info!("lok: first tile drawn"); } window.with_content_mask(Some(ContentMask { bounds }), |window| { for rect in painted.selection { window.paint_quad(fill(rect, selection_color)); } for (mark, color) in painted.marks { window.paint_quad(quad( mark, Corners::default(), transparent_black(), Edges::all(px(2.0)), color, BorderStyle::Solid, )); } if let Some(cell) = painted.cell { window.paint_quad(quad( cell, Corners::default(), transparent_black(), Edges::all(px(2.0)), frame_color, BorderStyle::Solid, )); } if let Some((frame, handles)) = painted.frame { window.paint_quad(outline(frame, frame_color, BorderStyle::Solid)); for handle in handles { window.paint_quad(quad( handle, Corners::default(), handle_fill, Edges::all(px(1.0)), frame_color, BorderStyle::Solid, )); } } if let Some(caret) = painted.caret.filter(|_| focus.is_focused(window)) { let caret = Bounds { size: size(caret_width, caret.size.height), ..caret }; window.paint_quad(fill(caret, caret_color)); } }); window.handle_input(&focus, ElementInputHandler::new(bounds, this), app); }, ) .size_full(), ) .children(self.hints(cx)) } } /// The page mask, tiles, selection and caret to paint. pub(super) struct Painted { /// Page frames on screen; tiles show only inside them. pub(super) pages: Vec>, /// Each tile, its place and the pane that clips it. pub(super) tiles: Vec<(Bounds, Arc, Bounds)>, pub(super) selection: Vec>, pub(super) caret: Option>, /// A spreadsheet's cell cursor. pub(super) cell: Option>, /// The selected object's frame and handle squares. pub(super) frame: Option<(Bounds, Vec>)>, /// The references of a formula being typed. pub(super) marks: Vec<(Bounds, gpui::Hsla)>, } impl LokView { /// The zoom that fits the page, with a margin. fn fit_zoom(&mut self) { const MARGIN: f32 = 0.94; let (w, h) = self.doc_size; if w <= 0 || h <= 0 || self.viewport.0 <= 0.0 || self.viewport.1 <= 0.0 { return; } let across = self.viewport.0 * TWIPS_PER_PIXEL / w as f32; let down = self.viewport.1 * TWIPS_PER_PIXEL / h as f32; let zoom = (across.min(down) * MARGIN).min(4.0); if (zoom - self.geom.zoom).abs() > 0.001 { self.keep_scroll(|view| view.geom.zoom = zoom); self.retire_tiles(); self.failed.clear(); } } /// All its images, for a closing tab to free. pub fn retire_all(&mut self) -> ely_gpui_component::primitives::Retired { self.retire_tiles(); std::mem::take(&mut self.retired) } /// Every tile goes; their images are freed next paint. pub(super) fn retire_tiles(&mut self) { for (_, tile) in self.tiles.drain() { self.retired.push(tile.image); } } /// Drops tiles over a screen away from any shown one. fn keep_near(&mut self, (vw, vh): (i32, i32)) { let reach = (vw / TILE_TWIPS + 1, vh / TILE_TWIPS + 1); let shown = &self.on_screen; let before = self.tiles.len(); let retired = &mut self.retired; self.tiles.retain(|key, tile| { let near = shown .iter() .any(|at| (key.0 - at.0).abs() <= reach.0 && (key.1 - at.1).abs() <= reach.1); if !near { retired.push(tile.image.clone()); } near }); if self.tiles.len() < before { log::debug!("lok: dropped {} far tiles", before - self.tiles.len()); } } /// The page mask and visible tiles; requests the missing ones. pub(super) fn prepare(&mut self, bounds: Bounds) -> Painted { self.viewport = (bounds.size.width.into(), bounds.size.height.into()); self.origin = bounds.origin; if self.fit { self.fit_zoom(); } self.clamp_scroll(); let geom = self.geom; let (sx, sy) = self.scroll; // A narrow document sits centered. let extent = self.extent(); let slack = |view: f32, doc: i32| (view - geom.px_of(doc)).max(0.0) / 2.0; // A fitted page sits centred both ways. self.spare = ( slack(self.viewport.0, extent.0), if self.fit { slack(self.viewport.1, extent.1) } else { 0.0 }, ); let pages = if self.pages.is_empty() { vec![self.tile_screen_rect((0, 0, extent.0, extent.1))] } else { self.pages .iter() .map(|&page| self.screen_rect(page)) .collect() }; let (vw, vh) = ( geom.twips_of(bounds.size.width.into()), geom.twips_of(bounds.size.height.into()), ); let scale = (geom.tile_pixels(), TILE_TWIPS); if self.shown != Some(((sx, sy, vw, vh), scale)) { self.shown = Some(((sx, sy, vw, vh), scale)); self.failed.clear(); self.send(input::Input::Visible((sx, sy, vw, vh), scale)); } let (ax, ay) = self.axes(); let side = px(geom.px_of(TILE_TWIPS)); let at = |vx: i32, vy: i32| { bounds.origin + point( px(self.spare.0 + geom.px_of(vx)), px(self.spare.1 + geom.px_of(vy)), ) }; let mut ready = Vec::new(); let mut missing = Vec::new(); self.on_screen.clear(); for (dx0, dx1, vx) in ax.panes(vw) { for (dy0, dy1, vy) in ay.panes(vh) { let clip = Bounds::from_corners(at(vx, vy), at(vx + dx1 - dx0, vy + dy1 - dy0)); let (kx0, ky0) = (dx0.div_euclid(TILE_TWIPS), dy0.div_euclid(TILE_TWIPS)); let (kx1, ky1) = (dx1.div_euclid(TILE_TWIPS), dy1.div_euclid(TILE_TWIPS)); for key in (kx0..=kx1).flat_map(|kx| (ky0..=ky1).map(move |ky| (kx, ky))) { if key.0 * TILE_TWIPS >= extent.0 || key.1 * TILE_TWIPS >= extent.1 { continue; } self.on_screen.push(key); match self.tiles.get(&key) { Some(tile) => { let origin = at(vx + key.0 * TILE_TWIPS - dx0, vy + key.1 * TILE_TWIPS - dy0); let place = Bounds { origin, size: size(side, side), }; ready.push((place, Arc::clone(&tile.image), clip)); if tile.stale { missing.push(key); } } None => missing.push(key), } } } } self.keep_near((vw, vh)); for key in missing { self.request_tile(key); } let rect = |rect: Rect| self.screen_rect(rect); let caret = self.cursor.filter(|_| self.cursor_visible).map(rect); let half = self.handle_size / 2.0; let frame = self.frame().map(|frame| { let squares = handles::points(frame) .iter() .map(|&(x, y)| Bounds { origin: self.window_point((x, y)) - point(half, half), size: size(self.handle_size, self.handle_size), }) .collect(); (rect(frame), squares) }); Painted { pages, tiles: ready, selection: self.selection.iter().copied().map(rect).collect(), caret, cell: self.cell.cursor.map(rect), frame, marks: self.marks(), } } }