363 lines
15 KiB
Rust
363 lines
15 KiB
Rust
//! 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<Self>) -> 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<Bounds<Pixels>>,
|
|
/// Each tile, its place and the pane that clips it.
|
|
pub(super) tiles: Vec<(Bounds<Pixels>, Arc<RenderImage>, Bounds<Pixels>)>,
|
|
pub(super) selection: Vec<Bounds<Pixels>>,
|
|
pub(super) caret: Option<Bounds<Pixels>>,
|
|
/// A spreadsheet's cell cursor.
|
|
pub(super) cell: Option<Bounds<Pixels>>,
|
|
/// The selected object's frame and handle squares.
|
|
pub(super) frame: Option<(Bounds<Pixels>, Vec<Bounds<Pixels>>)>,
|
|
/// The references of a formula being typed.
|
|
pub(super) marks: Vec<(Bounds<Pixels>, 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());
|
|
}
|
|
}
|
|
|
|
/// Room beside a narrow document, which sits centred.
|
|
pub(super) fn slack(&self) -> (f32, f32) {
|
|
// A sheet's grid grows with the view; it starts left.
|
|
if self.doc.is_sheet() {
|
|
return (0.0, 0.0);
|
|
}
|
|
let extent = self.extent();
|
|
let slack = |view: f32, doc: i32| (view - self.geom.px_of(doc)).max(0.0) / 2.0;
|
|
// A fitted page sits centred both ways.
|
|
let down = if self.fit { slack(self.viewport.1, extent.1) } else { 0.0 };
|
|
(slack(self.viewport.0, extent.0), down)
|
|
}
|
|
|
|
/// The page mask and visible tiles; requests the missing ones.
|
|
pub(super) fn prepare(&mut self, bounds: Bounds<Pixels>) -> 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;
|
|
let extent = self.extent();
|
|
self.spare = self.slack();
|
|
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(),
|
|
}
|
|
}
|
|
}
|