//! The drawing canvas: document raster, overlay, grid, page border and //! pointer input. use crate::theme::Tokens; use crate::App; use egui::{pos2, vec2, Color32, Pos2, Rect, Sense, Shape, Stroke, TextureHandle, TextureOptions}; use rustfig_engine::{Button, Overlay, ToolEvent}; use rustfig_geom::{Affine, Point}; use rustfig_model::{Document, ObjectId, ObjectKind}; use rustfig_render::raster::{rasterize, RasterOptions}; use rustfig_render::{Scene, SceneOptions}; #[derive(Debug, Clone, PartialEq)] struct DocKey { revision: u64, zoom: u64, ox: u64, oy: u64, w: usize, h: usize, hide: Vec, } impl DocKey { /// The view the raster was made for. fn view(&self, ppp: f32) -> rustfig_engine::View { let mut v = rustfig_engine::View { zoom: f64::from_bits(self.zoom), origin: Point::new(f64::from_bits(self.ox), f64::from_bits(self.oy)), ..Default::default() }; v.size = (self.w as f64 / f64::from(ppp), self.h as f64 / f64::from(ppp)); v } /// Same drawing, maybe another view. fn same_drawing(&self, o: &DocKey) -> bool { self.revision == o.revision && self.hide == o.hide } } /// A document raster being made on a background thread (A7). #[cfg(not(target_arch = "wasm32"))] struct Job { key: DocKey, /// The raster and how long it took (ms). rx: std::sync::mpsc::Receiver<(egui::ColorImage, f64)>, } #[derive(Default)] pub struct CanvasState { doc_tex: Option, /// What `doc_tex` shows (it may lag behind the view while a new raster /// is made; it is then drawn moved and scaled to the current view). doc_key: Option, /// The scene of the current drawing, kept across pans and zooms. scene: Option<(DocKey, std::sync::Arc)>, #[cfg(not(target_arch = "wasm32"))] job: Option, ov_tex: Option, ov_key: Option<(Overlay, DocKey)>, /// Pointer position in Fig units, if over the canvas. pub cursor: Option, last_sent: Option, /// The tool button of a press that started on the canvas. pressing: Option<(egui::PointerButton, Button)>, /// Panning with space+drag. panning: bool, /// Milliseconds the last document render took. pub render_ms: f64, /// A touch held down, waiting to become a right click. long_press: LongPress, } /// Seconds a touch must stay put to count as a right click. const LONG_PRESS_SECS: f64 = 0.5; /// Points a touch may drift and still be a long press. const LONG_PRESS_SLOP: f32 = 8.0; /// Touch long-press detector: a touch held for `LONG_PRESS_SECS` without /// moving more than `LONG_PRESS_SLOP` points acts as the right button. #[derive(Default, Clone, Copy, Debug, PartialEq)] pub(crate) struct LongPress { start: Option<(Pos2, f64)>, fired: bool, } impl LongPress { pub(crate) fn begin(&mut self, pos: Pos2, now: f64) { self.start = Some((pos, now)); self.fired = false; } pub(crate) fn moved(&mut self, pos: Pos2) { if let Some((p0, _)) = self.start { if (pos - p0).length() > LONG_PRESS_SLOP { self.start = None; } } } /// The touch ended; true if the long press had fired (the lift must /// then not count as a left click). pub(crate) fn end(&mut self) -> bool { let f = self.fired; *self = Self::default(); f } pub(crate) fn cancel(&mut self) { self.start = None; } /// The press position once, when the hold time has passed. pub(crate) fn poll(&mut self, now: f64) -> Option { let (p, t) = self.start?; if now - t >= LONG_PRESS_SECS { self.start = None; self.fired = true; return Some(p); } None } pub(crate) fn pending(&self) -> bool { self.start.is_some() } } impl CanvasState { /// Drop cached rasters so the next frame redraws everything. pub fn invalidate(&mut self) { self.doc_key = None; self.ov_key = None; self.scene = None; } } /// Side of an object marker in points. const MARKER: f32 = 7.0; fn to_pos(p: Point, origin: Pos2) -> Pos2 { pos2(origin.x + p.x as f32, origin.y + p.y as f32) } /// The scene of `doc` minus `hide`. fn build_scene(doc: &Document, opts: &SceneOptions, hide: &[ObjectId]) -> Scene { if hide.is_empty() { rustfig_render::build_scene(doc, opts) } else { let mut d = Document { settings: doc.settings.clone(), comments: Vec::new(), export_on_save: doc.export_on_save.clone(), colors: doc.colors.clone(), objects: Vec::with_capacity(doc.objects.len()), next_id: 0, open: doc.open.clone(), }; d.objects = doc.objects.iter().filter(|o| !hide.contains(&o.id)).cloned().collect(); rustfig_render::build_scene(&d, opts) } } /// A render that took this long or more goes to a background thread next /// time (native), so pans and zooms never wait for it. const SYNC_RENDER_MS: f64 = 20.0; /// Show `img`, the raster for `key`. fn set_doc_texture(app: &mut App, ctx: &egui::Context, img: egui::ColorImage, key: DocKey) { match &mut app.canvas.doc_tex { Some(tex) => tex.set(img, TextureOptions::NEAREST), None => app.canvas.doc_tex = Some(ctx.load_texture("doc", img, TextureOptions::NEAREST)), } app.canvas.doc_key = Some(key); } fn rasterize_scene(scene: &Scene, t: Affine, w: usize, h: usize) -> egui::ColorImage { let Some(mut pm) = tiny_skia_pixmap(w, h) else { return egui::ColorImage::new([1, 1], vec![Color32::TRANSPARENT]); }; rasterize( scene, &mut pm, &RasterOptions { transform: t, min_line_px: 1.0, adjust_px: 0.0, pattern_k: 1, pattern: rustfig_render::pattern::PatternFrame::screen(), }, ); egui::ColorImage::from_rgba_premultiplied([w, h], pm.data()) } /// The grid step (Fig units) and its spacing on screen (points). As xfig's /// `setup_grid` (w_grid.c), a grid too fine for the zoom shows the next /// coarser one; the caller hides it when even the coarsest is under 5 px. fn grid_spacing(g: rustfig_engine::SnapMode, units: rustfig_model::Units, px_per_fig: f64) -> (f64, f64) { let all = rustfig_engine::SnapMode::ALL; let from = all.iter().position(|&s| s == g).unwrap_or(0); let mut best = (0.0, 0.0); for s in all.iter().skip(from) { let step = s.step(units); if step <= 0.0 { continue; } best = (step, step * px_per_fig); if best.1 >= 5.0 { break; } } best } /// Top-left corners of the pages that touch the visible region, in every /// direction from the origin (multiple-page mode); none above 2,000 pages. pub(crate) fn page_tiles(page: (f64, f64), vis: rustfig_geom::Rect) -> Vec<(f64, f64)> { let (w, h) = page; let mut out = Vec::new(); if !(w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0) { return out; } let (i0, i1) = ((vis.min.x / w).floor(), (vis.max.x / w).ceil()); let (j0, j1) = ((vis.min.y / h).floor(), (vis.max.y / h).ceil()); if !(i0.is_finite() && i1.is_finite() && j0.is_finite() && j1.is_finite()) || (i1 - i0) * (j1 - j0) > 2000.0 { return out; } let mut j = j0; while j < j1 { let mut i = i0; while i < i1 { out.push((i * w, j * h)); i += 1.0; } j += 1.0; } out } fn tiny_skia_pixmap(w: usize, h: usize) -> Option { rustfig_render::raster::Pixmap::new(w.clamp(1, 16_384) as u32, h.clamp(1, 16_384) as u32) } /// The raster's pixels per point: the screen's, but no more than fits the /// biggest texture (`max_side` pixels) for a canvas `longest_side` points long. pub fn raster_scale(ppp: f32, longest_side: f32, max_side: usize) -> f32 { let limit = max_side as f32 / longest_side.max(1.0); if ppp.is_finite() && ppp > 0.0 && limit.is_finite() { // A little under, so rounding up never passes the limit. ppp.min(limit * 0.999) } else { 1.0 } } /// Most objects named in the canvas description. const MAX_DESCRIBED: usize = 8; /// Longest text quoted in the description, in characters. const MAX_QUOTED: usize = 24; /// What a screen reader says for the canvas (G3): the object count and the /// first few objects by kind, e.g. `Canvas: 3 objects: polyline, box, text 'Hello'`. pub fn describe_drawing(doc: &Document) -> String { use rustfig_model::{EllipseKind, ObjectKind, PolyKind}; let n = doc.objects.len(); if n == 0 { return "Canvas: empty drawing".to_string(); } let names: Vec = doc .objects .iter() .take(MAX_DESCRIBED) .map(|o| match &o.kind { ObjectKind::Ellipse(e) => match e.kind { EllipseKind::CircleRadius | EllipseKind::CircleDiameter => "circle".to_string(), _ => "ellipse".to_string(), }, ObjectKind::Polyline(p) => match p.kind { PolyKind::Polyline => "polyline", PolyKind::Box => "box", PolyKind::Polygon => "polygon", PolyKind::ArcBox => "arc-box", PolyKind::Picture => "picture", } .to_string(), ObjectKind::Spline(_) => "spline".to_string(), ObjectKind::Arc(_) => "arc".to_string(), ObjectKind::Compound(c) => format!("compound of {}", c.objects.len()), ObjectKind::Text(t) => { let line = t.text.lines().next().unwrap_or(""); let quoted: String = line.chars().take(MAX_QUOTED).collect(); let dots = if line.chars().count() > MAX_QUOTED { "..." } else { "" }; format!("text '{quoted}{dots}'") } }) .collect(); let more = if n > MAX_DESCRIBED { format!(", and {} more", n - MAX_DESCRIBED) } else { String::new() }; let noun = if n == 1 { "object" } else { "objects" }; format!("Canvas: {n} {noun}: {}{more}", names.join(", ")) } pub fn show(app: &mut App, ui: &mut egui::Ui, tk: &Tokens) { let area = ui.available_rect_before_wrap(); // xfig's rulers: top, right side, and the units box in the corner. let (rect, top, side, unit_box) = crate::rulers::layout(area); let resp = ui.allocate_rect(rect, Sense::click_and_drag()); // G3: screen readers get a summary of the drawing (the picture itself is // a texture). let description = describe_drawing(app.engine.doc()); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Other, true, description.clone())); let painter = ui.painter_at(rect); // Physical pixels per point for the raster, lowered when the canvas would // be larger than the graphics driver's biggest texture (a big window on // a 4K or 5K screen with a weak GPU: 2048 or 4096 pixels): the picture is // then a little soft, but there (P2). let ppp = raster_scale( ui.ctx().pixels_per_point(), rect.width().max(rect.height()), ui.ctx().input(|i| i.max_texture_side), ); app.engine.view.size = (f64::from(rect.width()), f64::from(rect.height())); let view = app.engine.view; let origin = rect.min; let to_screen = |p: Point| to_pos(view.to_screen(p), origin); painter.rect_filled(rect, 0.0, tk.canvas_bg); // Paper and page border, as xfig's `redisplay_pageborder` (u_redraw.c): // the page's outline with the paper's name outside its lower-right // corner; in multiple-page mode every page's outline. Without the border // the canvas is all paper, as in xfig. let doc = app.engine.doc(); let page = doc.page_size(); let border = app.engine.show_page_border; if !border || doc.settings.multiple_pages { painter.rect_filled(rect, 0.0, tk.paper); } else { let paper = Rect::from_two_pos(to_screen(Point::ZERO), to_screen(Point::new(page.0, page.1))); painter.rect_filled(paper, 0.0, tk.paper); } if border { let stroke = Stroke::new(1.0_f32, tk.page_border); let pages = if doc.settings.multiple_pages { page_tiles(page, view.visible()) } else { vec![(0.0, 0.0)] }; for (x, y) in pages { let r = Rect::from_two_pos( to_screen(Point::new(x, y)), to_screen(Point::new(x + page.0, y + page.1)), ); painter.rect_stroke(r, 0.0, stroke, egui::StrokeKind::Middle); } painter.text( to_screen(Point::new(page.0, page.1)) + vec2(3.0, 0.0), egui::Align2::LEFT_BOTTOM, doc.settings.paper.name(), egui::FontId::proportional(11.0), tk.page_border, ); } // Grid. if let Some(g) = app.engine.indicator.grid { let (step, px) = grid_spacing(g, doc.settings.units, view.px_per_fig()); if step > 0.0 && px >= 5.0 { let vis = view.visible(); let stroke = Stroke::new(1.0_f32, tk.grid); if app.engine.indicator.grid_iso { // xfig's isometric grid (w_grid.c): vertical lines `step * cos 30` // apart and diagonals at plus and minus 30 degrees, a step apart. let xs = step * 0.75_f64.sqrt(); let slope = 30.0_f64.to_radians().tan(); let mut i = (vis.min.x / xs).floor(); let mut n = 0; while i * xs <= vis.max.x && n < 4000 { let sx = to_screen(Point::new(i * xs, 0.0)).x; painter.line_segment([pos2(sx, rect.top()), pos2(sx, rect.bottom())], stroke); i += 1.0; n += 1; } let (x0, x1) = (vis.min.x, vis.max.x); for sign in [1.0, -1.0] { let (lo, hi) = if sign > 0.0 { (vis.min.y - x1 * slope, vis.max.y - x0 * slope) } else { (vis.min.y + x0 * slope, vis.max.y + x1 * slope) }; let mut k = (lo / step).floor(); let mut n = 0; while k * step <= hi && n < 4000 { let y = k * step; let a = to_screen(Point::new(x0, y + sign * x0 * slope)); let b = to_screen(Point::new(x1, y + sign * x1 * slope)); painter.line_segment([pos2(a.x, a.y), pos2(b.x, b.y)], stroke); k += 1.0; n += 1; } } } else { let mut x = (vis.min.x / step).floor() * step; let mut n = 0; while x <= vis.max.x && n < 4000 { let sx = to_screen(Point::new(x, 0.0)).x; painter.line_segment([pos2(sx, rect.top()), pos2(sx, rect.bottom())], stroke); x += step; n += 1; } let mut y = (vis.min.y / step).floor() * step; while y <= vis.max.y && n < 8000 { let sy = to_screen(Point::new(0.0, y)).y; painter.line_segment([pos2(rect.left(), sy), pos2(rect.right(), sy)], stroke); y += step; n += 1; } } } } // Axis lines through the origin (xfig's "Draw axis lines", pink). if app.engine.show_axis_lines() { let stroke = Stroke::new(1.0_f32, Color32::from_rgb(255, 192, 203)); let o = to_screen(Point::new(0.0, 0.0)); if o.x >= rect.left() && o.x <= rect.right() { painter.line_segment([pos2(o.x, rect.top()), pos2(o.x, rect.bottom())], stroke); } if o.y >= rect.top() && o.y <= rect.bottom() { painter.line_segment([pos2(rect.left(), o.y), pos2(rect.right(), o.y)], stroke); } } // Document raster (physical pixels for crisp output). let overlay = app.engine.overlay(); let (w, h) = ( (rect.width() * ppp).round().max(1.0) as usize, (rect.height() * ppp).round().max(1.0) as usize, ); let key = DocKey { revision: app.engine.revision(), zoom: view.zoom.to_bits(), ox: view.origin.x.to_bits(), oy: view.origin.y.to_bits(), w, h, hide: overlay.hide.clone(), }; let t = view.transform().then(&Affine::scale(f64::from(ppp), f64::from(ppp))); app.engine.poll_latex(); if app.engine.latex.pending() > 0 { ui.ctx().request_repaint_after(std::time::Duration::from_millis(150)); } let scene_opts = SceneOptions { typeset: app.engine.latex.typesetter(), hidden_depths: app.engine.hidden_depths.clone(), gray_hidden: app.engine.gray_hidden_depths, screen: true, text_scale: None, // The canvas shows TeX's own output, never the approximation. approx_latex: false, }; // The scene: rebuilt only when the drawing changes, not on pan or zoom. let scene = match &app.canvas.scene { Some((k, sc)) if k.same_drawing(&key) => std::sync::Arc::clone(sc), _ => { let sc = std::sync::Arc::new(build_scene(app.engine.doc(), &scene_opts, &overlay.hide)); app.canvas.scene = Some((key.clone(), std::sync::Arc::clone(&sc))); sc } }; // A background render that finished: show it (the newest view wins). #[cfg(not(target_arch = "wasm32"))] if let Some(job) = &app.canvas.job { match job.rx.try_recv() { Ok((img, ms)) => { let k = job.key.clone(); app.canvas.job = None; app.canvas.render_ms = ms; if k.same_drawing(&key) { set_doc_texture(app, ui.ctx(), img, k); } } Err(std::sync::mpsc::TryRecvError::Disconnected) => app.canvas.job = None, Err(std::sync::mpsc::TryRecvError::Empty) => {} } } if app.canvas.doc_key.as_ref() != Some(&key) { #[cfg(not(target_arch = "wasm32"))] let busy = app.canvas.job.is_some(); #[cfg(target_arch = "wasm32")] let busy = false; // Slow drawings render in the background (native); while a render // runs, the next starts when it is done. let background = cfg!(not(target_arch = "wasm32")) && app.canvas.render_ms >= SYNC_RENDER_MS && app.canvas.doc_tex.is_some(); if background { #[cfg(not(target_arch = "wasm32"))] if !busy { let (tx, rx) = std::sync::mpsc::channel(); let (sc, ctx, k) = (std::sync::Arc::clone(&scene), ui.ctx().clone(), key.clone()); let spawned = std::thread::Builder::new() .name("rustfig-render".into()) .spawn(move || { let start = std::time::Instant::now(); let img = rasterize_scene(&sc, t, w, h); let ms = start.elapsed().as_secs_f64() * 1000.0; // The canvas may be gone (app closing): nothing to do. let _ = tx.send((img, ms)); ctx.request_repaint(); }); match spawned { Ok(_) => app.canvas.job = Some(Job { key: k, rx }), Err(_) => { let img = rasterize_scene(&scene, t, w, h); set_doc_texture(app, ui.ctx(), img, key.clone()); } } } #[cfg(target_arch = "wasm32")] let _ = busy; } else if !busy { let start = web_time_now(); let img = rasterize_scene(&scene, t, w, h); app.canvas.render_ms = web_time_now() - start; set_doc_texture(app, ui.ctx(), img, key.clone()); } } let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)); if let (Some(tex), Some(shown)) = (&app.canvas.doc_tex, &app.canvas.doc_key) { // The raster where its view puts it in the current one: exact when // the views agree, moved and scaled while a new raster is made. let old = shown.view(ppp); let tl = view.to_screen(old.to_fig(Point::ZERO)); let br = view.to_screen(old.to_fig(Point::new(old.size.0, old.size.1))); let r = Rect::from_min_max(to_pos(tl, origin), to_pos(br, origin)); painter.image(tex.id(), r, uv, Color32::WHITE); } // Overlay raster (previews, text being typed). let has_overlay = !overlay.preview.is_empty() || overlay.text.is_some(); if has_overlay { let ok = app.canvas.ov_key.as_ref() == Some(&(overlay.clone(), key.clone())); if !ok { let doc = app.engine.doc(); let mut d = Document { settings: doc.settings.clone(), comments: Vec::new(), export_on_save: doc.export_on_save.clone(), colors: doc.colors.clone(), objects: overlay.preview.iter().cloned().map(std::sync::Arc::new).collect(), next_id: 0, open: Vec::new(), }; // Objects being placed or moved look as on the canvas (TeX's // picture where it has one); the text being typed shows its source. let mut scene = rustfig_render::build_scene( &d, &SceneOptions { typeset: scene_opts.typeset.clone(), screen: true, ..SceneOptions::default() }, ); if let Some(t) = &overlay.text { d.objects = vec![std::sync::Arc::new(t.object.clone())]; let typed = rustfig_render::build_scene(&d, &SceneOptions::default()); scene.prims.extend(typed.prims); } let img = rasterize_scene(&scene, t, w, h); match &mut app.canvas.ov_tex { Some(tex) => tex.set(img, TextureOptions::NEAREST), None => app.canvas.ov_tex = Some(ui.ctx().load_texture("overlay", img, TextureOptions::NEAREST)), } app.canvas.ov_key = Some((overlay.clone(), key)); } if let Some(tex) = &app.canvas.ov_tex { painter.image(tex.id(), rect, uv, Color32::from_white_alpha(230)); } } // Guides, selection, handles. let guide = Stroke::new(1.0_f32, tk.guide); for g in &overlay.guides { let pts: Vec = g.iter().map(|p| to_screen(*p)).collect(); painter.extend(Shape::dashed_line(&pts, guide, 4.0, 3.0)); } // Show vertex numbers (xfig's `shownums`): the point's number beside it. if app.engine.show_vertex_numbers() { let vis = view.visible(); for (p, n) in app.engine.vertex_numbers() { if p.x >= vis.min.x && p.x <= vis.max.x && p.y >= vis.min.y && p.y <= vis.max.y { painter.text( to_screen(p) + egui::vec2(3.0, -3.0), egui::Align2::LEFT_BOTTOM, n.to_string(), egui::FontId::monospace(10.0), Color32::from_rgb(255, 0, 0), // xfig draws them in red ); } } } // The crosshair resource: full-window red lines through the pointer. if let Some(c) = overlay.crosshair { let (p, clip) = (to_screen(c), painter.clip_rect()); let stroke = egui::Stroke::new(1.0_f32, Color32::from_rgb(255, 0, 0)); painter.line_segment([egui::pos2(clip.min.x, p.y), egui::pos2(clip.max.x, p.y)], stroke); painter.line_segment([egui::pos2(p.x, clip.min.y), egui::pos2(p.x, clip.max.y)], stroke); } // Show Lengths: the sides' lengths in red (xfig draws them in RED). for (p, s) in &overlay.lengths { painter.text( to_screen(*p), egui::Align2::LEFT_BOTTOM, s, egui::FontId::monospace(10.0), Color32::from_rgb(255, 0, 0), ); } for h in &overlay.handles { let p = to_screen(*h); let r = Rect::from_center_size(p, vec2(6.0, 6.0)); painter.rect_filled(r, 0.0, tk.handle_fill); painter.rect_stroke(r, 0.0, Stroke::new(1.0_f32, tk.handle_stroke), egui::StrokeKind::Middle); } // xfig's object markers: hollow squares on what the mode can act on // (xfig's 5 pixels at 80 dpi; 7 points here, readable on today's // screens); a tagged (selected) object's are filled (`toggle_*highlight`). for m in &overlay.markers { let p = to_screen(*m); if !rect.expand(MARKER).contains(p) { continue; } let r = Rect::from_center_size(p, vec2(MARKER, MARKER)); painter.rect_stroke(r, 0.0, Stroke::new(1.0_f32, tk.handle_stroke), egui::StrokeKind::Middle); } for m in &overlay.tagged { let p = to_screen(*m); if rect.expand(MARKER).contains(p) { painter.rect_filled(Rect::from_center_size(p, vec2(MARKER, MARKER)), 0.0, tk.handle_stroke); } } // Chop: the axes in one colour, the log object under the pointer (what // the middle button cuts) in another. let chop = overlay.chop_axes.iter().map(|h| (h, tk.chop_axe)); for (h, colour) in chop.chain(overlay.chop_log.iter().map(|h| (h, tk.chop_log))) { let outline: Vec = h.outline.iter().map(|p| to_screen(*p)).collect(); painter.extend(Shape::dashed_line(&outline, Stroke::new(1.5_f32, colour), 5.0, 3.0)); for m in &h.markers { let r = Rect::from_center_size(to_screen(*m), vec2(MARKER, MARKER)); painter.rect_filled(r, 0.0, colour); } } // Join/Split: a magenta square around the first end point (xfig's // `draw_join_marker`, 20 pixels). if let Some(p) = overlay.join_marker { let r = Rect::from_center_size(to_screen(p), vec2(20.0, 20.0)); painter.rect_stroke( r, 0.0, Stroke::new(1.0_f32, Color32::from_rgb(255, 0, 255)), egui::StrokeKind::Middle, ); } if let Some(ot) = &overlay.text { draw_caret(&painter, ot, &to_screen, tk); } handle_input(app, ui, &resp, rect); crate::rulers::show(app, ui, tk, rect, top, side, unit_box); for r in [top, side, unit_box] { app.context_help.zones.push((r, crate::context_help::RULER_TOPIC)); } } fn draw_caret( painter: &egui::Painter, ot: &rustfig_engine::tools::OverlayText, to_screen: &dyn Fn(Point) -> Pos2, tk: &Tokens, ) { let ObjectKind::Text(t) = &ot.object.kind else { return }; let em = t.size * rustfig_geom::FIG_PER_POINT; let rot = Affine::rotate(t.angle); let lines: Vec<&str> = t.text.split('\n').collect(); // Top and bottom of the caret at character `idx` (screen points). let at = |idx: usize| -> (Pos2, Pos2) { let mut rest = idx; let mut li = 0; for (i, l) in lines.iter().enumerate() { li = i; let n = l.chars().count(); if rest <= n || i + 1 == lines.len() { rest = rest.min(n); break; } rest -= n + 1; } let line = lines.get(li).copied().unwrap_or(""); let prefix: String = line.chars().take(rest).collect(); let pw = rustfig_engine::measure_in_font(t, &prefix).width; let lw = rustfig_engine::measure_in_font(t, line).width; let x0 = match t.justify { rustfig_model::Justify::Left => 0.0, rustfig_model::Justify::Center => -lw / 2.0, rustfig_model::Justify::Right => -lw, }; let y = li as f64 * em * rustfig_text::LINE_SPACING; let a = t.origin + rot.apply_vec(Point::new(x0 + pw, y + em * 0.2)); let b = t.origin + rot.apply_vec(Point::new(x0 + pw, y - em * 0.8)); (to_screen(a), to_screen(b)) }; // E10: the selection, one quad per line it touches. if let Some((s0, s1)) = ot.selection { let mut start = 0; for l in &lines { let n = l.chars().count(); let (a, b) = (s0.max(start), s1.min(start + n)); if a < b { let (a0, a1) = at(a); let (b0, b1) = at(b); painter.add(egui::Shape::convex_polygon( vec![a0, b0, b1, a1], tk.caret.gamma_multiply(0.3), Stroke::NONE, )); } start += n + 1; } } // The IME composition, underlined. let (ps, pl) = ot.preedit; if pl > 0 { let (a0, _) = at(ps); let (b0, _) = at(ps + pl); painter.line_segment([a0, b0], Stroke::new(1.5_f32, tk.caret)); } let (a, b) = at(ot.cursor); painter.line_segment([a, b], Stroke::new(1.5_f32, tk.caret)); // Tell the system where the caret is, for the IME's candidate window. let r = Rect::from_two_pos(a, b); painter.ctx().output_mut(|o| { o.ime = Some(egui::output::IMEOutput { rect: r.expand(40.0), cursor_rect: r, }); }); } fn web_time_now() -> f64 { #[cfg(not(target_arch = "wasm32"))] { use std::sync::OnceLock; static START: OnceLock = OnceLock::new(); START.get_or_init(std::time::Instant::now).elapsed().as_secs_f64() * 1000.0 } #[cfg(target_arch = "wasm32")] { 0.0 } } /// The tool button for a physical button: Alt+left is the middle button and /// Ctrl+left the right one, for two-button mice and trackpads. pub fn tool_button(b: egui::PointerButton, m: egui::Modifiers) -> Option