git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/widget/doc_editor/mod.rs (54.9K)

   1 //! `DocEditor`: a document editor with Markdown live preview — cce-notes'
   2 //! editing mode (Obsidian-on-cce milestone 6), and a fast plain editor with
   3 //! preview off.
   4 //!
   5 //! - [`buffer`]: the text as lines, caret and selection, undoable edits.
   6 //! - [`preview`]: how a line shows — markup hidden except on the caret's
   7 //!   line, where it shows dimmed (Obsidian's live preview).
   8 //! - [`layout`]: one styled line wrapped into runs, with the x of every
   9 //!   byte, so drawing, caret and clicks agree.
  10 //!
  11 //! **Incremental.** A line is laid out (shaped) only when it is drawn and
  12 //! has changed: an edit invalidates the lines it replaced
  13 //! ([`buffer::Change`]), a moved caret the lines it left and entered (their
  14 //! markers show or hide), a fence the block contexts after it. Lines never
  15 //! drawn keep an estimated height, so opening a long file shapes one
  16 //! screen of it.
  17 //!
  18 //! **Driven by its host, not a registered widget.** The app forwards keys,
  19 //! pointer events and the wheel, and paints it into its display list
  20 //! ([`DocEditor::paint`]); answers come back as [`Response`] — a link to
  21 //! follow is the host's to resolve. The caret does not blink: a blinking
  22 //! caret is a frame every half second for as long as the window is open.
  23 
  24 pub mod buffer;
  25 pub mod layout;
  26 pub mod preview;
  27 
  28 use std::time::Duration;
  29 use web_time::Instant;
  30 
  31 pub use buffer::{Buffer, EditKind, Pos};
  32 pub use layout::EditorTheme;
  33 pub use preview::Target;
  34 
  35 use crate::scene::layout::Rect;
  36 use crate::scene::paint::{Cap, PaintCtx};
  37 use crate::widget::shaping::ShapingMeasure;
  38 use crate::widget::{Bounds, EmbedImage, Key, KeyEvent, MouseScrollDelta, NamedKey, ScrollMotion};
  39 use layout::{Deco, LineLayout};
  40 use preview::{Context, Kind, Marker, Prop};
  41 
  42 /// What a key or click did, for the host.
  43 #[derive(Clone, Debug, PartialEq)]
  44 pub enum Response {
  45     None,
  46     /// The caret or selection moved; the text did not change.
  47     Moved,
  48     /// The text changed.
  49     Changed,
  50     /// A link was clicked (rendered) or Ctrl+clicked (raw).
  51     Follow(Target),
  52 }
  53 
  54 const DOUBLE_CLICK: Duration = Duration::from_millis(400);
  55 
  56 pub struct DocEditor {
  57     pub buf: Buffer,
  58     /// Live preview: markup hidden except on the caret's lines. Off: every
  59     /// line shows as written (with its syntax still dimmed).
  60     pub preview: bool,
  61     pub theme: EditorTheme,
  62     /// The widest the text column grows (Obsidian's readable line length);
  63     /// 0 lets it fill the rect.
  64     pub max_width: f32,
  65     pub pad: f32,
  66     measure: ShapingMeasure,
  67     ctx: Vec<Context>,
  68     /// The frontmatter lines' Properties roles (`None` elsewhere).
  69     props: Vec<Option<Prop>>,
  70     layouts: Vec<Option<LineLayout>>,
  71     heights: Vec<f32>,
  72     tops: Vec<f32>,
  73     tops_dirty: bool,
  74     /// Lines laid out as active (raw), so a caret move relays out the lines
  75     /// it leaves and the ones it enters.
  76     shown_active: (usize, usize),
  77     width: f32,
  78     pub scroll: f32,
  79     motion: ScrollMotion,
  80     /// The x a vertical move keeps to.
  81     want_x: Option<f32>,
  82     dragging: bool,
  83     clicks: Option<(Instant, Pos, u8)>,
  84     /// Where the text column was last painted: its origin and the rect.
  85     origin: (f32, f32),
  86     viewport: Rect,
  87     follow_caret: bool,
  88     /// The host's embedded images, by link text (see [`DocEditor::set_images`]).
  89     images: Option<Box<dyn Fn(&str) -> Option<EmbedImage>>>,
  90     /// An input method's composition (see `crate::ime`), at the source
  91     /// position it began, and the `ime::generation` it was taken at. It is
  92     /// in the caret line's LAYOUT, never in the buffer — the buffer is what
  93     /// the host saves — so every column read off that line's layout is
  94     /// mapped across it (`laid_col`, `source_col`).
  95     composition: Option<(Pos, crate::ime::Preedit)>,
  96     ime_seen: u64,
  97 }
  98 
  99 impl DocEditor {
 100     /// An editor over `text`. `system_fonts` must match the app's
 101     /// `Application::load_system_fonts`, so measured widths are drawn ones.
 102     pub fn new(text: &str, theme: EditorTheme, system_fonts: bool) -> DocEditor {
 103         let mut e = DocEditor {
 104             buf: Buffer::new(text),
 105             preview: true,
 106             theme,
 107             max_width: 0.0,
 108             pad: 24.0,
 109             measure: ShapingMeasure::new(system_fonts),
 110             ctx: Vec::new(),
 111             props: Vec::new(),
 112             layouts: Vec::new(),
 113             heights: Vec::new(),
 114             tops: Vec::new(),
 115             tops_dirty: true,
 116             shown_active: (usize::MAX, usize::MAX),
 117             width: 0.0,
 118             scroll: 0.0,
 119             motion: ScrollMotion::new(),
 120             want_x: None,
 121             dragging: false,
 122             clicks: None,
 123             origin: (0.0, 0.0),
 124             viewport: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
 125             follow_caret: false,
 126             images: None,
 127             composition: None,
 128             ime_seen: 0,
 129         };
 130         e.sync();
 131         e.caret_to_body();
 132         e
 133     }
 134 
 135     pub fn set_text(&mut self, text: &str) {
 136         self.buf.set_text(text);
 137         self.scroll = 0.0;
 138         self.motion = ScrollMotion::new();
 139         self.want_x = None;
 140         self.sync();
 141         self.caret_to_body();
 142     }
 143 
 144     /// Start the caret on the body, past a frontmatter block, so the
 145     /// Properties table shows rather than its raw YAML.
 146     fn caret_to_body(&mut self) {
 147         if let Some(close) = self.props.iter().position(|p| *p == Some(Prop::Close)) {
 148             self.buf.caret = Pos::new((close + 1).min(self.buf.line_count() - 1), 0);
 149             self.sync();
 150         }
 151     }
 152 
 153     pub fn text(&self) -> String {
 154         self.buf.text()
 155     }
 156 
 157     /// Where embedded images come from: `images` answers an embed's link
 158     /// text with the host's uploaded image, or `None` (not an image, or
 159     /// not loaded yet — the line shows as text). In live preview a line
 160     /// that is one embed shows as its image, and with the caret on it as
 161     /// the raw text with the image below. Asked at layout for the size and
 162     /// again at paint for the id; call [`DocEditor::invalidate`] when an
 163     /// image arrives or changes size.
 164     pub fn set_images(&mut self, images: Box<dyn Fn(&str) -> Option<EmbedImage>>) {
 165         self.images = Some(images);
 166         self.invalidate();
 167     }
 168 
 169     /// Drop every layout (fonts or the theme changed).
 170     pub fn invalidate(&mut self) {
 171         self.layouts.fill(None);
 172         self.tops_dirty = true;
 173     }
 174 
 175     pub fn set_preview(&mut self, on: bool) {
 176         if self.preview != on {
 177             self.preview = on;
 178             self.invalidate();
 179         }
 180     }
 181 
 182     // ---- incremental bookkeeping ----------------------------------------
 183 
 184     /// The lines shown raw: the caret's (or the selection's), widened to
 185     /// a fenced code block or the frontmatter the caret is in, so its
 186     /// fences (or its raw YAML) show.
 187     fn active_range(&self) -> (usize, usize) {
 188         let (mut a, mut b) = match self.buf.selection() {
 189             Some((a, b)) => (a.line, b.line),
 190             None => (self.buf.caret.line, self.buf.caret.line),
 191         };
 192         // The frontmatter shows raw while the caret is anywhere in it, as
 193         // a fenced block does; elsewhere it is the Properties table.
 194         if let Some((x, _)) = preview::frontmatter_block(&self.ctx, a) {
 195             a = x;
 196         }
 197         if let Some((_, y)) = preview::frontmatter_block(&self.ctx, b) {
 198             b = y;
 199         }
 200         if let Some((x, _)) = preview::fenced_block(&self.ctx, a) {
 201             a = x;
 202         }
 203         if let Some((_, y)) = preview::fenced_block(&self.ctx, b) {
 204             b = y;
 205         }
 206         (a, b)
 207     }
 208 
 209     fn is_active(&self, i: usize, act: (usize, usize)) -> bool {
 210         !self.preview || (i >= act.0 && i <= act.1)
 211     }
 212 
 213     fn estimate(&self, i: usize) -> f32 {
 214         let row = (self.theme.size * self.theme.spacing).ceil();
 215         let chars = self.buf.line(i).chars().count() as f32;
 216         let per_row = (self.width / (self.theme.size * 0.5)).max(10.0);
 217         row * (chars / per_row).ceil().max(1.0)
 218     }
 219 
 220     /// Bring layouts, contexts and heights in line with the buffer.
 221     fn sync(&mut self) {
 222         for c in self.buf.take_changes() {
 223             if c.removed == usize::MAX {
 224                 self.layouts = vec![None; self.buf.line_count()];
 225                 self.heights = vec![0.0; self.buf.line_count()];
 226                 self.ctx.clear();
 227                 continue;
 228             }
 229             let end = (c.first + c.removed).min(self.layouts.len());
 230             self.layouts.splice(c.first..end, std::iter::repeat_with(|| None).take(c.inserted));
 231             self.heights.splice(c.first..end, std::iter::repeat_n(0.0, c.inserted));
 232             self.tops_dirty = true;
 233         }
 234         let n = self.buf.line_count();
 235         self.layouts.resize_with(n, || None);
 236         self.heights.resize(n, 0.0);
 237         let ctx = preview::contexts(self.buf.lines());
 238         if ctx.len() == self.ctx.len() {
 239             for i in 0..n {
 240                 if ctx[i] != self.ctx[i] {
 241                     self.layouts[i] = None;
 242                 }
 243             }
 244         } else {
 245             self.layouts.fill(None);
 246         }
 247         self.ctx = ctx;
 248         let props = preview::properties(self.buf.lines(), &self.ctx);
 249         for (i, p) in props.iter().enumerate() {
 250             if self.props.get(i) != Some(p) {
 251                 self.layouts[i] = None;
 252             }
 253         }
 254         self.props = props;
 255         let act = self.active_range();
 256         if act != self.shown_active {
 257             let (a, b) = self.shown_active;
 258             for i in [a, b, act.0, act.1] {
 259                 if i < n {
 260                     self.layouts[i] = None;
 261                 }
 262             }
 263             // A selection's middle lines too.
 264             for i in act.0.min(n)..=act.1.min(n.saturating_sub(1)) {
 265                 self.layouts[i] = None;
 266             }
 267             if a != usize::MAX {
 268                 for i in a.min(n)..=b.min(n.saturating_sub(1)) {
 269                     self.layouts[i] = None;
 270                 }
 271             }
 272             self.shown_active = act;
 273         }
 274         for i in 0..n {
 275             let h = match &self.layouts[i] {
 276                 Some(l) => l.height,
 277                 None if self.heights[i] > 0.0 => self.heights[i],
 278                 None => self.estimate(i),
 279             };
 280             if (h - self.heights[i]).abs() > 0.01 {
 281                 self.heights[i] = h;
 282                 self.tops_dirty = true;
 283             }
 284         }
 285         self.retop();
 286     }
 287 
 288     fn retop(&mut self) {
 289         if !self.tops_dirty && self.tops.len() == self.heights.len() + 1 {
 290             return;
 291         }
 292         self.tops.clear();
 293         self.tops.reserve(self.heights.len() + 1);
 294         let mut y = 0.0;
 295         for h in &self.heights {
 296             self.tops.push(y);
 297             y += h;
 298         }
 299         self.tops.push(y);
 300         self.tops_dirty = false;
 301     }
 302 
 303     fn ensure(&mut self, i: usize) {
 304         if i >= self.layouts.len() || self.layouts[i].is_some() {
 305             return;
 306         }
 307         let act = self.shown_active;
 308         let active = self.is_active(i, act);
 309         // The caret line with a composition in it, laid out as typed.
 310         let composed = self.composition.as_ref().filter(|(at, _)| at.line == i).map(|(at, p)| {
 311             let mut t = self.buf.line(i).to_string();
 312             t.insert_str(at.col.min(t.len()), &p.text);
 313             t
 314         });
 315         let text = composed.as_deref().unwrap_or(self.buf.line(i));
 316         let line = match self.props.get(i) {
 317             Some(Some(p)) if !active => {
 318                 let key = match p {
 319                     Prop::Item { key_line: Some(k), .. } => preview::prop_key(self.buf.line(*k)),
 320                     _ => None,
 321                 };
 322                 preview::style_property(text, p, key)
 323             }
 324             _ => preview::style_line(text, self.ctx[i], active),
 325         };
 326         let mut l = layout::layout_line(text, &line, active, self.width, &self.theme, &mut self.measure);
 327         if self.preview && self.ctx[i] == Context::Normal {
 328             if let Some((target, want_w, want_h)) = preview::standalone_embed(text) {
 329                 if let Some(img) = self.images.as_ref().and_then(|f| f(&target)) {
 330                     let (w, h) = img.fit(want_w, want_h, self.width);
 331                     l = if active { l.with_image_below(&target, w, h) } else { LineLayout::image(&target, w, h, l.text_size) };
 332                 }
 333             } else if text.contains("![") {
 334                 // Embeds inside the text: their pictures in a row below it.
 335                 let width = self.width;
 336                 let found: Vec<(String, f32, f32)> = preview::inline_embeds(text)
 337                     .into_iter()
 338                     .filter_map(|(target, want_w, want_h)| {
 339                         let img = self.images.as_ref().and_then(|f| f(&target))?;
 340                         let (w, h) = img.fit(want_w, want_h, width);
 341                         Some((target, w, h))
 342                     })
 343                     .collect();
 344                 l = l.with_images_below(&found, width);
 345             }
 346         }
 347         if (l.height - self.heights[i]).abs() > 0.01 {
 348             self.heights[i] = l.height;
 349             self.tops_dirty = true;
 350         }
 351         self.layouts[i] = Some(l);
 352     }
 353 
 354     fn line_at_y(&self, y: f32) -> usize {
 355         let n = self.heights.len();
 356         match self.tops[..n].binary_search_by(|t| t.partial_cmp(&y).unwrap_or(std::cmp::Ordering::Less)) {
 357             Ok(i) => i,
 358             Err(0) => 0,
 359             Err(i) => (i - 1).min(n - 1),
 360         }
 361     }
 362 
 363     pub fn content_height(&self) -> f32 {
 364         self.tops.last().copied().unwrap_or(0.0) + 2.0 * self.pad
 365     }
 366 
 367     fn max_scroll(&self) -> f32 {
 368         (self.content_height() - self.viewport.height).max(0.0)
 369     }
 370 
 371     // ---- the input method ---------------------------------------------------
 372 
 373     /// Take up the input method's composition, if it has moved since the
 374     /// editor last did: the caret line relaid out with it in. A composition
 375     /// begun over a selection replaces it, as typing would.
 376     fn sync_ime(&mut self) {
 377         let generation = crate::ime::generation();
 378         if generation == self.ime_seen {
 379             return;
 380         }
 381         self.ime_seen = generation;
 382         let next = crate::ime::preedit();
 383         if next.is_some() && self.composition.is_none() && self.buf.delete_selection() {
 384             self.sync();
 385         }
 386         if let Some((at, _)) = self.composition.take() {
 387             self.relayout(at.line);
 388         }
 389         if let Some(p) = next {
 390             let at = self.buf.caret;
 391             self.buf.anchor = None;
 392             self.composition = Some((at, p));
 393             self.relayout(at.line);
 394             self.follow_caret = true;
 395         }
 396     }
 397 
 398     /// The editor lost the keyboard, or a press moved its caret: a
 399     /// composition it was showing is dropped, and the input method asked to
 400     /// cancel it.
 401     pub fn drop_composition(&mut self) {
 402         if let Some((at, _)) = self.composition.take() {
 403             self.relayout(at.line);
 404             crate::ime::request_reset();
 405         }
 406         self.ime_seen = crate::ime::generation();
 407     }
 408 
 409     /// Whether an input method's composition is showing.
 410     pub fn composing(&self) -> bool {
 411         self.composition.is_some()
 412     }
 413 
 414     fn relayout(&mut self, line: usize) {
 415         if let Some(l) = self.layouts.get_mut(line) {
 416             *l = None;
 417         }
 418         self.tops_dirty = true;
 419     }
 420 
 421     /// The composition on `line`: its source column, its length in bytes,
 422     /// and the bytes into it the input method has its cursor.
 423     fn composition_on(&self, line: usize) -> Option<(usize, usize, usize)> {
 424         let (at, p) = self.composition.as_ref().filter(|(at, _)| at.line == line)?;
 425         let caret = p.text.char_indices().nth(p.caret_chars()).map_or(p.text.len(), |(b, _)| b);
 426         Some((at.col, p.text.len(), caret))
 427     }
 428 
 429     /// A source column of `line` as a column of its layout: past a
 430     /// composition there, moved over it; the caret itself, where the input
 431     /// method has its cursor in it.
 432     fn laid_col(&self, line: usize, col: usize, is_caret: bool) -> usize {
 433         match self.composition_on(line) {
 434             Some((c, _, caret)) if is_caret && col == c => c + caret,
 435             Some((c, len, _)) if col > c => col + len,
 436             _ => col,
 437         }
 438     }
 439 
 440     /// A column of `line`'s layout as a source column: inside a composition
 441     /// is where it began.
 442     fn source_col(&self, line: usize, col: usize) -> usize {
 443         match self.composition_on(line) {
 444             Some((c, len, _)) if col > c && col < c + len => c,
 445             Some((c, len, _)) if col >= c + len => col - len,
 446             _ => col,
 447         }
 448     }
 449 
 450     // ---- caret geometry -----------------------------------------------------
 451 
 452     /// The caret's (x, y) in content space and its row height.
 453     fn caret_point(&mut self, p: Pos) -> (f32, f32, f32) {
 454         self.ensure(p.line);
 455         self.retop();
 456         let col = self.laid_col(p.line, p.col, p == self.buf.caret);
 457         let l = self.layouts[p.line].as_ref().expect("laid out");
 458         let (x, row) = l.caret_xy(col);
 459         (x, self.tops[p.line] + row as f32 * l.row_h, l.row_h)
 460     }
 461 
 462     /// The caret's rect on screen as last painted — for a popup at it.
 463     pub fn caret_rect(&mut self) -> Rect {
 464         let (x, y, h) = self.caret_point(self.buf.caret);
 465         Rect { x: self.origin.0 + x, y: self.origin.1 + y - self.scroll, width: 2.0, height: h }
 466     }
 467 
 468     /// The position under a screen point.
 469     /// The text position under a window point (as painted last), for a
 470     /// host placing something there — a drop.
 471     pub fn pos_at(&mut self, sx: f32, sy: f32) -> Pos {
 472         let (x, y) = (sx - self.origin.0, sy - self.origin.1 + self.scroll);
 473         if y < 0.0 {
 474             return Pos::default();
 475         }
 476         let i = self.line_at_y(y);
 477         self.ensure(i);
 478         self.retop();
 479         let l = self.layouts[i].as_ref().unwrap();
 480         let row = (((y - self.tops[i]) / l.row_h).floor().max(0.0) as usize).min(l.rows - 1);
 481         let col = l.col_at(x, row);
 482         Pos::new(i, self.source_col(i, col))
 483     }
 484 
 485     fn scroll_to_caret(&mut self) {
 486         let (_, y, h) = self.caret_point(self.buf.caret);
 487         let view = self.viewport.height - 2.0 * self.pad;
 488         if view <= 0.0 {
 489             return;
 490         }
 491         if y < self.scroll {
 492             self.scroll = y;
 493         } else if y + h > self.scroll + view {
 494             self.scroll = y + h - view;
 495         }
 496         self.scroll = self.scroll.clamp(0.0, self.max_scroll());
 497         self.motion.y.jump_to(self.scroll);
 498     }
 499 
 500     /// Move the caret one visual row (or `rows`) up or down, keeping `want_x`.
 501     fn vertical(&mut self, rows: i32, select: bool) {
 502         let p = self.buf.caret;
 503         self.ensure(p.line);
 504         let (cx, row) = self.layouts[p.line].as_ref().unwrap().caret_xy(p.col);
 505         let x = *self.want_x.get_or_insert(cx);
 506         let (mut line, mut row) = (p.line as i64, row as i64);
 507         let mut left = rows.unsigned_abs();
 508         while left > 0 {
 509             row += rows.signum() as i64;
 510             let rows_here = self.layouts[line as usize].as_ref().map_or(1, |l| l.rows) as i64;
 511             if row < 0 {
 512                 if line == 0 {
 513                     row = 0;
 514                     break;
 515                 }
 516                 line -= 1;
 517                 self.ensure(line as usize);
 518                 row = self.layouts[line as usize].as_ref().unwrap().rows as i64 - 1;
 519             } else if row >= rows_here {
 520                 if line as usize + 1 >= self.buf.line_count() {
 521                     row = rows_here - 1;
 522                     break;
 523                 }
 524                 line += 1;
 525                 self.ensure(line as usize);
 526                 row = 0;
 527             }
 528             left -= 1;
 529         }
 530         let l = self.layouts[line as usize].as_ref().unwrap();
 531         let col = l.col_at(x, row as usize);
 532         let keep = self.want_x;
 533         self.buf.set_caret(Pos::new(line as usize, col), select);
 534         self.want_x = keep;
 535     }
 536 
 537     // ---- input --------------------------------------------------------------
 538 
 539     /// A key press. Undo/redo chords are the host's to route (the runner
 540     /// sends them to `Application::undo`); call [`DocEditor::undo`] there.
 541     pub fn key(&mut self, ev: &KeyEvent) -> Response {
 542         self.sync();
 543         // While an input method composes, its keys are its own: a shell does
 544         // not deliver them, and one that does is not editing this text.
 545         self.sync_ime();
 546         if self.composition.is_some() {
 547             return Response::None;
 548         }
 549         let rev = self.buf.revision;
 550         let caret = (self.buf.caret, self.buf.anchor);
 551         let (ctrl, shift) = (ev.ctrl, ev.shift);
 552         let mut keep_x = false;
 553         match &ev.logical_key {
 554             Key::Named(NamedKey::ArrowLeft) => {
 555                 let to = match (self.buf.selection(), shift, ctrl) {
 556                     (Some((a, _)), false, false) => a,
 557                     (_, _, true) => self.buf.word_left(self.buf.caret),
 558                     _ => self.buf.prev(self.buf.caret),
 559                 };
 560                 self.buf.set_caret(to, shift);
 561             }
 562             Key::Named(NamedKey::ArrowRight) => {
 563                 let to = match (self.buf.selection(), shift, ctrl) {
 564                     (Some((_, b)), false, false) => b,
 565                     (_, _, true) => self.buf.word_right(self.buf.caret),
 566                     _ => self.buf.next(self.buf.caret),
 567                 };
 568                 self.buf.set_caret(to, shift);
 569             }
 570             Key::Named(NamedKey::ArrowUp) => {
 571                 self.vertical(-1, shift);
 572                 keep_x = true;
 573             }
 574             Key::Named(NamedKey::ArrowDown) => {
 575                 self.vertical(1, shift);
 576                 keep_x = true;
 577             }
 578             Key::Named(NamedKey::PageUp) | Key::Named(NamedKey::PageDown) => {
 579                 let row = (self.theme.size * self.theme.spacing).max(1.0);
 580                 let rows = ((self.viewport.height - 2.0 * self.pad) / row).floor().max(1.0) as i32;
 581                 let dir = if matches!(ev.logical_key, Key::Named(NamedKey::PageUp)) { -1 } else { 1 };
 582                 self.vertical(dir * rows, shift);
 583                 keep_x = true;
 584             }
 585             Key::Named(NamedKey::Home) => {
 586                 let to = if ctrl {
 587                     Pos::default()
 588                 } else {
 589                     // Smart home: the content start first, then column 0.
 590                     let p = self.buf.caret;
 591                     self.ensure(p.line);
 592                     let start = self.content_start(p.line);
 593                     Pos::new(p.line, if p.col > start { start } else { 0 })
 594                 };
 595                 self.buf.set_caret(to, shift);
 596             }
 597             Key::Named(NamedKey::End) => {
 598                 let to = if ctrl { self.buf.end() } else { Pos::new(self.buf.caret.line, self.buf.line(self.buf.caret.line).len()) };
 599                 self.buf.set_caret(to, shift);
 600             }
 601             Key::Named(NamedKey::Backspace) => self.buf.backspace(ctrl),
 602             Key::Named(NamedKey::Delete) => self.buf.delete_forward(ctrl),
 603             Key::Named(NamedKey::Enter) => self.enter(),
 604             Key::Named(NamedKey::Tab) => self.tab(shift),
 605             Key::Named(NamedKey::Escape) => {
 606                 if self.buf.anchor.is_some() {
 607                     self.buf.anchor = None;
 608                 } else {
 609                     return Response::None;
 610                 }
 611             }
 612             Key::Character(c) if ctrl => match c.to_lowercase().as_str() {
 613                 "a" => self.buf.select_all(),
 614                 "c" => {
 615                     if let Some(t) = self.buf.selected_text() {
 616                         crate::widget::clipboard::copy_to_clipboard(&t);
 617                     }
 618                 }
 619                 "x" => {
 620                     if let Some(t) = self.buf.selected_text() {
 621                         crate::widget::clipboard::copy_to_clipboard(&t);
 622                         self.buf.delete_selection();
 623                     }
 624                 }
 625                 "v" => {
 626                     if let Some(t) = crate::widget::clipboard::read_from_clipboard() {
 627                         self.buf.insert(&t.replace("\r\n", "\n"), EditKind::Other);
 628                     }
 629                 }
 630                 "b" => self.wrap("**"),
 631                 "i" => self.wrap("*"),
 632                 _ => return Response::None,
 633             },
 634             _ => {
 635                 let Some(text) = ev.text.as_deref() else { return Response::None };
 636                 if ctrl || ev.alt || text.is_empty() || text.chars().any(|c| c.is_control()) {
 637                     return Response::None;
 638                 }
 639                 let kind = if text.chars().all(char::is_whitespace) { EditKind::Other } else { EditKind::Typing };
 640                 self.buf.insert(text, kind);
 641             }
 642         }
 643         if !keep_x {
 644             self.want_x = None;
 645         }
 646         self.after_edit(rev, caret)
 647     }
 648 
 649     fn after_edit(&mut self, rev: u64, caret: (Pos, Option<Pos>)) -> Response {
 650         if self.buf.revision != rev {
 651             self.sync();
 652             self.follow_caret = true;
 653             Response::Changed
 654         } else if (self.buf.caret, self.buf.anchor) != caret {
 655             self.sync();
 656             self.follow_caret = true;
 657             Response::Moved
 658         } else {
 659             Response::None
 660         }
 661     }
 662 
 663     fn content_start(&self, line: usize) -> usize {
 664         preview::style_line(self.buf.line(line), self.ctx.get(line).copied().unwrap_or(Context::Normal), true).content_start
 665     }
 666 
 667     /// Enter continues a list item (`- `, `1. ` counting on, `- [ ] `),
 668     /// and on an empty item ends the list instead, as Obsidian does.
 669     fn enter(&mut self) {
 670         let p = self.buf.caret;
 671         let text = self.buf.line(p.line).to_string();
 672         let ctx = self.ctx.get(p.line).copied().unwrap_or(Context::Normal);
 673         let line = preview::style_line(&text, ctx, true);
 674         if self.buf.selection().is_none() {
 675             if let Kind::List { marker, .. } = &line.kind {
 676                 let body = &text[line.content_start..];
 677                 if body.trim().is_empty() && p.col >= line.content_start {
 678                     // An empty item: drop its marker, leave the list.
 679                     self.buf.replace(Pos::new(p.line, 0), Pos::new(p.line, text.len()), "", EditKind::Other);
 680                     return;
 681                 }
 682                 let indent: String = text.chars().take_while(|c| *c == ' ' || *c == '\t').collect();
 683                 let next = match marker {
 684                     Marker::Bullet => format!("{indent}{} ", text.trim_start().chars().next().unwrap_or('-')),
 685                     Marker::Number(n) => {
 686                         let digits: String = n.chars().filter(char::is_ascii_digit).collect();
 687                         let delim = n.chars().last().unwrap_or('.');
 688                         format!("{indent}{}{delim} ", digits.parse::<u64>().unwrap_or(0) + 1)
 689                     }
 690                     Marker::Task(..) => {
 691                         let bullet = text.trim_start().chars().next().unwrap_or('-');
 692                         format!("{indent}{bullet} [ ] ")
 693                     }
 694                 };
 695                 if p.col >= line.content_start {
 696                     self.buf.insert(&format!("\n{next}"), EditKind::Other);
 697                     return;
 698                 }
 699             }
 700         }
 701         self.buf.insert("\n", EditKind::Other);
 702     }
 703 
 704     /// Tab indents a list item (or the selected lines) by a tab; Shift+Tab
 705     /// takes one level off. Anywhere else Tab inserts a tab.
 706     fn tab(&mut self, outdent: bool) {
 707         let (a, b) = self.buf.selection().unwrap_or((self.buf.caret, self.buf.caret));
 708         let is_list = |s: &Self, i: usize| matches!(preview::style_line(s.buf.line(i), s.ctx.get(i).copied().unwrap_or(Context::Normal), true).kind, Kind::List { .. });
 709         if a.line == b.line && !is_list(self, a.line) && !outdent {
 710             self.buf.insert("\t", EditKind::Other);
 711             return;
 712         }
 713         let (caret, anchor) = (self.buf.caret, self.buf.anchor);
 714         let mut caret_delta = 0isize;
 715         let mut anchor_delta = 0isize;
 716         for i in a.line..=b.line {
 717             let text = self.buf.line(i).to_string();
 718             let d: isize = if outdent {
 719                 let n = if text.starts_with('\t') {
 720                     1
 721                 } else {
 722                     text.chars().take(4).take_while(|c| *c == ' ').count()
 723                 };
 724                 if n == 0 {
 725                     continue;
 726                 }
 727                 self.buf.replace(Pos::new(i, 0), Pos::new(i, n), "", EditKind::Other);
 728                 -(n as isize)
 729             } else {
 730                 self.buf.replace(Pos::new(i, 0), Pos::new(i, 0), "\t", EditKind::Other);
 731                 1
 732             };
 733             if i == caret.line {
 734                 caret_delta = d;
 735             }
 736             if anchor.is_some_and(|x| x.line == i) {
 737                 anchor_delta = d;
 738             }
 739         }
 740         let fix = |p: Pos, d: isize| Pos::new(p.line, (p.col as isize + d).max(0) as usize);
 741         self.buf.caret = self.buf.clamp(fix(caret, caret_delta));
 742         self.buf.anchor = anchor.map(|x| self.buf.clamp(fix(x, anchor_delta)));
 743     }
 744 
 745     /// Wrap the selection in `mark` (Ctrl+B, Ctrl+I), or insert a pair
 746     /// with the caret between.
 747     fn wrap(&mut self, mark: &str) {
 748         match self.buf.selection() {
 749             Some((a, b)) if a.line == b.line => {
 750                 let inner = self.buf.text_range(a, b);
 751                 self.buf.replace(a, b, &format!("{mark}{inner}{mark}"), EditKind::Other);
 752                 self.buf.anchor = Some(Pos::new(a.line, a.col + mark.len()));
 753                 self.buf.caret = Pos::new(a.line, a.col + mark.len() + inner.len());
 754             }
 755             Some(_) => {}
 756             None => {
 757                 let at = self.buf.caret;
 758                 self.buf.insert(&format!("{mark}{mark}"), EditKind::Other);
 759                 self.buf.caret = Pos::new(at.line, at.col + mark.len());
 760             }
 761         }
 762     }
 763 
 764     /// Replace `a..b` with `text` as one undo step and put the caret at
 765     /// `caret` (a host's splice: a completion, a template).
 766     pub fn edit(&mut self, a: Pos, b: Pos, text: &str, caret: Pos) {
 767         self.buf.replace(a, b, text, EditKind::Other);
 768         self.buf.anchor = None;
 769         self.buf.caret = self.buf.clamp(caret);
 770         self.want_x = None;
 771         self.sync();
 772         self.follow_caret = true;
 773     }
 774 
 775     pub fn undo(&mut self) -> bool {
 776         let done = self.buf.undo();
 777         if done {
 778             self.sync();
 779             self.follow_caret = true;
 780         }
 781         done
 782     }
 783 
 784     pub fn redo(&mut self) -> bool {
 785         let done = self.buf.redo();
 786         if done {
 787             self.sync();
 788             self.follow_caret = true;
 789         }
 790         done
 791     }
 792 
 793     /// A left press at a screen point. A rendered link follows; a task's
 794     /// box toggles; Ctrl+click follows a raw link; otherwise the caret
 795     /// moves (Shift extends; a double press selects a word, a triple the
 796     /// line).
 797     pub fn press(&mut self, sx: f32, sy: f32, shift: bool, ctrl: bool) -> Response {
 798         self.sync();
 799         let rev = self.buf.revision;
 800         let before = (self.buf.caret, self.buf.anchor);
 801         // A press mid-composition places the caret, read through the line
 802         // as it is drawn, and the composition is left there, cancelled.
 803         if self.composition.is_some() {
 804             let p = self.pos_at(sx, sy);
 805             self.drop_composition();
 806             self.buf.set_caret(p, false);
 807             self.dragging = true;
 808             self.want_x = None;
 809             return self.after_edit(rev, before);
 810         }
 811         let (x, y) = (sx - self.origin.0, sy - self.origin.1 + self.scroll);
 812         if y >= 0.0 && !shift {
 813             let i = self.line_at_y(y);
 814             self.ensure(i);
 815             self.retop();
 816             let active = self.is_active(i, self.shown_active);
 817             let l = self.layouts[i].as_ref().unwrap();
 818             let ly = y - self.tops[i];
 819             if let Some((r, at)) = l.task {
 820                 if r.width > 0.0 && x >= r.x && x <= r.x + r.width && ly >= r.y && ly <= r.y + r.height {
 821                     let status = self.buf.line(i)[at..].chars().next().unwrap_or(' ');
 822                     let next = if status == ' ' { "x" } else { " " };
 823                     let end = at + status.len_utf8();
 824                     let keep = (self.buf.caret, self.buf.anchor);
 825                     self.buf.replace(Pos::new(i, at), Pos::new(i, end), next, EditKind::Other);
 826                     (self.buf.caret, self.buf.anchor) = keep;
 827                     return self.after_edit(rev, before);
 828                 }
 829             }
 830             if let Some((r, bytes, on)) = l.toggle.clone() {
 831                 if x >= r.x && x <= r.x + r.width && ly >= r.y && ly <= r.y + r.height {
 832                     // A boolean property's box: flip it in place.
 833                     let keep = (self.buf.caret, self.buf.anchor);
 834                     let next = if on { "false" } else { "true" };
 835                     self.buf.replace(Pos::new(i, bytes.start), Pos::new(i, bytes.end), next, EditKind::Other);
 836                     (self.buf.caret, self.buf.anchor) = keep;
 837                     return self.after_edit(rev, before);
 838                 }
 839             }
 840             if !active || ctrl {
 841                 if let Some(k) = l.link_at(x, ly) {
 842                     return Response::Follow(l.links[k].clone());
 843                 }
 844             }
 845         }
 846         let p = self.pos_at(sx, sy);
 847         let now = Instant::now();
 848         let count = match self.clicks {
 849             Some((t, at, n)) if now.duration_since(t) < DOUBLE_CLICK && at == p => n % 3 + 1,
 850             _ => 1,
 851         };
 852         self.clicks = Some((now, p, count));
 853         match count {
 854             2 => {
 855                 let (a, b) = self.buf.word_at(p);
 856                 self.buf.set_caret(a, false);
 857                 self.buf.set_caret(b, true);
 858             }
 859             3 => {
 860                 self.buf.set_caret(Pos::new(p.line, 0), false);
 861                 self.buf.set_caret(Pos::new(p.line, self.buf.line(p.line).len()), true);
 862             }
 863             _ => self.buf.set_caret(p, shift),
 864         }
 865         self.dragging = true;
 866         self.want_x = None;
 867         self.after_edit(rev, before)
 868     }
 869 
 870     /// Pointer motion; extends the selection while a press is held.
 871     pub fn drag(&mut self, sx: f32, sy: f32) -> Response {
 872         if !self.dragging {
 873             return Response::None;
 874         }
 875         let before = (self.buf.caret, self.buf.anchor);
 876         let p = self.pos_at(sx, sy);
 877         if p != self.buf.caret {
 878             self.buf.set_caret(p, true);
 879         }
 880         let r = self.after_edit(self.buf.revision, before);
 881         // Selecting by drag does not drag the view to the caret: only an
 882         // edge does, and the wheel.
 883         self.follow_caret = sy < self.viewport.y || sy > self.viewport.y + self.viewport.height;
 884         r
 885     }
 886 
 887     pub fn release(&mut self) {
 888         self.dragging = false;
 889     }
 890 
 891     /// Whether a press is being dragged (the host keeps routing motion).
 892     pub fn dragging(&self) -> bool {
 893         self.dragging
 894     }
 895 
 896     /// A link under a screen point, for a hover cursor.
 897     pub fn link_at(&mut self, sx: f32, sy: f32) -> bool {
 898         let (x, y) = (sx - self.origin.0, sy - self.origin.1 + self.scroll);
 899         if y < 0.0 || self.layouts.is_empty() {
 900             return false;
 901         }
 902         let i = self.line_at_y(y);
 903         if self.is_active(i, self.shown_active) {
 904             return false;
 905         }
 906         self.ensure(i);
 907         self.retop();
 908         let l = self.layouts[i].as_ref().unwrap();
 909         l.link_at(x, y - self.tops[i]).is_some() || l.task.is_some_and(|(r, _)| r.width > 0.0 && x >= r.x && x <= r.x + r.width && y - self.tops[i] <= r.y + r.height)
 910     }
 911 
 912     pub fn wheel(&mut self, delta: &MouseScrollDelta) -> bool {
 913         let line = (self.theme.size * self.theme.spacing).max(1.0);
 914         self.motion.reconcile(0.0, self.scroll);
 915         let moved = self.motion.apply(delta, (line, line * 2.0), Bounds::max(0.0), Bounds::max(self.max_scroll()));
 916         self.scroll = self.motion.y.pos();
 917         moved
 918     }
 919 
 920     /// Advance a wheel glide; true while it moves.
 921     pub fn tick(&mut self, dt: f32) -> bool {
 922         if !self.motion.is_animating() {
 923             return false;
 924         }
 925         self.motion.tick(dt, Bounds::max(0.0), Bounds::max(self.max_scroll()));
 926         self.scroll = self.motion.y.pos();
 927         true
 928     }
 929 
 930     /// Bring a line to the top of the view (search hits, outline clicks).
 931     pub fn reveal_line(&mut self, line: usize) {
 932         self.sync();
 933         let line = line.min(self.buf.line_count() - 1);
 934         self.retop();
 935         self.scroll = self.tops[line].clamp(0.0, self.max_scroll());
 936         self.motion.y.jump_to(self.scroll);
 937         self.buf.set_caret(Pos::new(line, 0), false);
 938         self.sync();
 939     }
 940 
 941     // ---- painting -------------------------------------------------------------
 942 
 943     /// Paint into `rect`: lay out what shows, then selection, text, caret.
 944     /// `focused` draws the caret.
 945     pub fn paint(&mut self, pc: &mut PaintCtx, rect: Rect, focused: bool) {
 946         self.prepare(rect);
 947         self.paint_prepared(pc, focused);
 948     }
 949 
 950     /// Lay out what shows in `rect` and settle the scroll, without
 951     /// painting: after it, [`DocEditor::caret_rect`] is where the caret
 952     /// will be drawn (a popup placed before the paint needs that).
 953     pub fn prepare(&mut self, rect: Rect) {
 954         let mut width = (rect.width - 2.0 * self.pad).max(40.0);
 955         if self.max_width > 0.0 {
 956             width = width.min(self.max_width);
 957         }
 958         if (width - self.width).abs() > 0.5 {
 959             self.width = width;
 960             self.invalidate();
 961             for i in 0..self.heights.len() {
 962                 self.heights[i] = 0.0;
 963             }
 964         }
 965         self.viewport = rect;
 966         self.origin = (rect.x + (rect.width - width) / 2.0, rect.y + self.pad);
 967         self.sync();
 968         self.sync_ime();
 969         if std::mem::take(&mut self.follow_caret) {
 970             self.scroll_to_caret();
 971         }
 972         // Lay out what shows (heights settle), then paint it.
 973         let view_h = rect.height;
 974         for _ in 0..2 {
 975             let mut i = self.line_at_y(self.scroll - self.pad);
 976             while i < self.buf.line_count() && self.tops[i] < self.scroll + view_h {
 977                 self.ensure(i);
 978                 i += 1;
 979             }
 980             self.retop();
 981         }
 982         self.scroll = self.scroll.clamp(0.0, self.max_scroll());
 983     }
 984 
 985     /// Paint what the last [`DocEditor::prepare`] laid out.
 986     pub fn paint_prepared(&mut self, pc: &mut PaintCtx, focused: bool) {
 987         self.paint_prepared_with(pc, focused, &|_| true);
 988     }
 989 
 990     /// [`DocEditor::paint_prepared`], with `resolved` saying whether a
 991     /// link has a target: one that does not draws faded, as in Obsidian.
 992     /// Asked at paint, so a vault change needs no relayout.
 993     pub fn paint_prepared_with(&mut self, pc: &mut PaintCtx, focused: bool, resolved: &dyn Fn(&Target) -> bool) {
 994         let rect = self.viewport;
 995         let view_h = rect.height;
 996         let (ox, oy) = (self.origin.0, self.origin.1 - self.scroll);
 997         let sel = self.buf.selection();
 998         let caret = self.buf.caret;
 999         let caret_col = self.laid_col(caret.line, caret.col, true);
1000         let composition = self.composition_on(caret.line);
1001         let th = self.theme.clone();
1002         // Focused is typing: the on-screen keyboard follows
1003         // (`crate::text_input`). The viewport stands in until the caret's
1004         // line is drawn below, and stays when it is scrolled out of view.
1005         if focused {
1006             let (dx, dy) = pc.offset();
1007             crate::text_input::claim(rect.x + dx, rect.y + dy, rect.width, rect.height);
1008         }
1009         let first = self.line_at_y(self.scroll - self.pad);
1010         pc.clip(rect, |pc| {
1011             let mut i = first;
1012             while i < self.buf.line_count() && self.tops[i] < self.scroll + view_h {
1013                 let Some(l) = self.layouts[i].as_ref() else {
1014                     i += 1;
1015                     continue;
1016                 };
1017                 let top = oy + self.tops[i];
1018                 // Selection, behind everything on the line.
1019                 // The boxes of the selected clusters, so a selection crossing a change of
1020                 // direction is drawn where its letters are.
1021                 if let Some((a, b)) = sel {
1022                     if i >= a.line && i <= b.line {
1023                         let from = if i == a.line { a.col } else { 0 };
1024                         let to = if i == b.line { b.col } else { usize::MAX };
1025                         for (row, x0, x1) in l.selection_rects(from, to, i < b.line) {
1026                             if x1 > x0 {
1027                                 pc.quad(Rect { x: ox + x0, y: top + row as f32 * l.row_h, width: x1 - x0, height: l.row_h }, th.selection);
1028                             }
1029                         }
1030                     }
1031                 }
1032                 for d in &l.decos {
1033                     match d {
1034                         Deco::Quad(r, c) => pc.quad(Rect { x: ox + r.x, y: top + r.y, width: r.width, height: r.height }, *c),
1035                         Deco::Dot { cx, cy, r, color } => pc.circle(ox + cx, top + cy, *r, *color),
1036                         Deco::Check { cx, cy, r, checked } => crate::widget::Checkbox::paint_inline(pc, ox + cx, top + cy, *r, *checked),
1037                         Deco::Text { text, x, y, size, color, font } => {
1038                             pc.text_with(text.clone(), ox + x, top + y, *size, srgb_u8(*color), Some(font.clone()), None)
1039                         }
1040                         Deco::Image { target, rect: r } => {
1041                             let at = Rect { x: ox + r.x, y: top + r.y, width: r.width, height: r.height };
1042                             match self.images.as_ref().and_then(|f| f(target)) {
1043                                 Some(img) => pc.image(img.id, at, 1.0),
1044                                 None => pc.rounded_rect(at, 4.0, (true, true, true, true), th.code_bg),
1045                             }
1046                         }
1047                     }
1048                 }
1049                 for r in &l.runs {
1050                     let row_y = top + r.row as f32 * l.row_h;
1051                     if let Some(bg) = r.bg {
1052                         let (pad, radius) = if r.look.pill { (layout::PILL_PAD, (l.row_h - 4.0) / 2.0) } else { (2.0, 3.0) };
1053                         pc.rounded_rect(Rect { x: ox + r.x - pad, y: row_y + 2.0, width: r.w + 2.0 * pad, height: l.row_h - 4.0 }, radius, (true, true, true, true), bg);
1054                     }
1055                     let ty = row_y + (l.row_h - r.size) / 2.0;
1056                     let color = match r.link {
1057                         Some(k) if !resolved(&l.links[k]) => th.link_unresolved,
1058                         _ => r.color,
1059                     };
1060                     pc.text_attrs(r.text.clone(), ox + r.x, ty, r.size, srgb_u8(color), Some(r.font.clone()), None, r.attrs);
1061                     if r.strike {
1062                         let sy = ty + r.size * 0.55;
1063                         pc.vector(ox + r.x, sy, ox + r.x + r.w, sy, 1.0, r.color, Cap::Flat);
1064                     }
1065                 }
1066                 if let Some((c, len, _)) = composition {
1067                     if i == caret.line {
1068                         // The composition's underline, a row at a time.
1069                         let (from, to) = (l.caret_xy(c), l.caret_xy(c + len));
1070                         for row in from.1..=to.1 {
1071                             let x0 = if row == from.1 { from.0 } else { l.content_x };
1072                             let x1 = if row == to.1 { to.0 } else { l.runs.iter().filter(|r| r.row == row).map(|r| r.x + r.w).fold(x0, f32::max) };
1073                             if x1 > x0 {
1074                                 let uy = top + (row + 1) as f32 * l.row_h - 3.0;
1075                                 pc.quad(Rect { x: ox + x0, y: uy, width: x1 - x0, height: 1.5 }, th.caret);
1076                             }
1077                         }
1078                     }
1079                 }
1080                 if focused && i == caret.line {
1081                     let (x, row) = l.caret_xy(caret_col);
1082                     let h = l.row_h * 0.8;
1083                     let y = top + row as f32 * l.row_h + (l.row_h - h) / 2.0;
1084                     pc.quad(Rect { x: ox + x - 0.5, y, width: 2.0, height: h }, th.caret);
1085                     // Where an input method puts its candidates, and that
1086                     // text is wanted at all.
1087                     let (dx, dy) = pc.offset();
1088                     crate::ime::report_caret(ox + x - 0.5 + dx, y + dy, 2.0, h);
1089                 }
1090                 i += 1;
1091             }
1092         });
1093     }
1094 }
1095 
1096 fn srgb_u8(linear: [f32; 4]) -> [u8; 3] {
1097     let s = crate::colors::to_srgb(linear);
1098     [(s[0] * 255.0) as u8, (s[1] * 255.0) as u8, (s[2] * 255.0) as u8]
1099 }
1100 
1101 #[cfg(test)]
1102 mod tests {
1103     use super::*;
1104     use crate::widget::ElementState;
1105 
1106     fn key(k: Key, text: Option<&str>, ctrl: bool, shift: bool) -> KeyEvent {
1107         KeyEvent { state: ElementState::Pressed, logical_key: k, text: text.map(String::from), repeat: false, ctrl, shift, alt: false }
1108     }
1109 
1110     fn typed(c: &str) -> KeyEvent {
1111         key(Key::Character(c.into()), Some(c), false, false)
1112     }
1113 
1114     fn named(n: NamedKey) -> KeyEvent {
1115         key(Key::Named(n), None, false, false)
1116     }
1117 
1118     fn editor(text: &str) -> DocEditor {
1119         let mut e = DocEditor::new(text, EditorTheme::new(14.0), false);
1120         let mut pc = PaintCtx::new();
1121         e.paint(&mut pc, Rect { x: 0.0, y: 0.0, width: 600.0, height: 400.0 }, true);
1122         e
1123     }
1124 
1125     /// A composition is in the caret line's layout and never in the buffer:
1126     /// drawn as typed and underlined, the caret where the input method has
1127     /// its cursor, columns read off the line mapped across it; keys wait
1128     /// while it is up; the commit is typed; a press drops it, asks the input
1129     /// method to cancel, and places the caret; the caret is reported.
1130     #[test]
1131     fn a_composition_is_laid_out_in_place_and_never_held() {
1132         use crate::ime::{self, Preedit};
1133         let rect = Rect { x: 0.0, y: 0.0, width: 600.0, height: 400.0 };
1134         let mut e = editor("ab\ncd");
1135         e.buf.caret = Pos::new(0, 1);
1136         e.buf.anchor = None;
1137         let mut pc = PaintCtx::new();
1138         e.paint(&mut pc, rect, true);
1139         let plain_caret = e.caret_rect().x;
1140         let _ = ime::take_reset();
1141 
1142         ime::set_preedit(Some(Preedit::new("にほ", Some((3, 3)))));
1143         ime::begin_frame();
1144         e.paint(&mut pc, rect, true);
1145         ime::end_frame();
1146         assert_eq!(e.text(), "ab\ncd", "never in the buffer");
1147         let l = e.layouts[0].as_ref().unwrap();
1148         let drawn: String = l.runs.iter().map(|r| r.text.as_str()).collect();
1149         assert_eq!(drawn, "aにほb");
1150         assert!(e.caret_rect().x > plain_caret, "the caret is after に");
1151         assert!(ime::caret().is_some(), "the caret is reported");
1152         // Past the composition on its line is the source's end of line.
1153         assert_eq!(e.pos_at(rect.x + 590.0, rect.y + e.pad + 2.0), Pos::new(0, 2));
1154         assert_eq!(e.key(&typed("x")), Response::None);
1155         assert_eq!(e.text(), "ab\ncd");
1156 
1157         // The commit: the composition ends, its text is typed.
1158         ime::set_preedit(None);
1159         assert_eq!(e.key(&typed("日本")), Response::Changed);
1160         assert_eq!(e.text(), "a日本b\ncd");
1161         assert_eq!(e.buf.caret, Pos::new(0, 7));
1162 
1163         // A press drops a composition, asks for it to be cancelled, and
1164         // places the caret.
1165         ime::set_preedit(Some(Preedit::new("か", None)));
1166         e.paint(&mut pc, rect, true);
1167         assert!(e.composing());
1168         e.press(rect.x + 590.0, rect.y + e.pad + 2.0, false, false);
1169         e.release();
1170         assert!(!e.composing());
1171         assert!(ime::take_reset());
1172         assert_eq!(e.text(), "a日本b\ncd");
1173         assert_eq!(e.buf.caret, Pos::new(0, "a日本b".len()));
1174     }
1175 
1176     #[test]
1177     fn an_embed_line_shows_its_image_and_the_raw_text_with_the_caret() {
1178         let mut e = DocEditor::new("top\n![[pic.png|200]]\nend", EditorTheme::new(14.0), false);
1179         e.set_images(Box::new(|t: &str| (t == "pic.png").then_some(EmbedImage { id: 3, width: 800, height: 400 })));
1180         let rect = Rect { x: 0.0, y: 0.0, width: 600.0, height: 400.0 };
1181         let mut pc = PaintCtx::new();
1182         e.paint(&mut pc, rect, true);
1183         let l = e.layouts[1].as_ref().unwrap();
1184         assert!(l.runs.is_empty(), "inactive: the image alone");
1185         assert!(matches!(&l.decos[0], Deco::Image { rect, .. } if rect.width == 200.0 && rect.height == 100.0));
1186         assert_eq!(l.height, 100.0 + 2.0 * layout::IMAGE_PAD);
1187         // The caret on it: raw text, image below.
1188         e.buf.caret = Pos::new(1, 0);
1189         e.buf.anchor = None;
1190         e.paint(&mut pc, rect, true);
1191         let l = e.layouts[1].as_ref().unwrap();
1192         assert!(!l.runs.is_empty());
1193         assert!(l.decos.iter().any(|d| matches!(d, Deco::Image { .. })));
1194         // Source mode: just text.
1195         e.set_preview(false);
1196         e.paint(&mut pc, rect, true);
1197         assert!(!e.layouts[1].as_ref().unwrap().decos.iter().any(|d| matches!(d, Deco::Image { .. })));
1198     }
1199 
1200     #[test]
1201     fn embeds_inside_a_line_show_below_it() {
1202         let mut e = DocEditor::new("text ![[a.png|100]] and ![[b.png|100]] more\nend", EditorTheme::new(14.0), false);
1203         e.set_images(Box::new(|_| Some(EmbedImage { id: 3, width: 200, height: 100 })));
1204         let mut pc = PaintCtx::new();
1205         e.buf.caret = Pos::new(1, 0);
1206         e.paint(&mut pc, Rect { x: 0.0, y: 0.0, width: 600.0, height: 400.0 }, true);
1207         let l = e.layouts[0].as_ref().unwrap();
1208         assert!(!l.runs.is_empty(), "the text stays");
1209         let rects: Vec<Rect> = l.decos.iter().filter_map(|d| match d {
1210             Deco::Image { rect, .. } => Some(*rect),
1211             _ => None,
1212         }).collect();
1213         assert_eq!(rects.len(), 2);
1214         assert_eq!((rects[0].width, rects[0].height), (100.0, 50.0));
1215         assert!(rects[1].x > rects[0].x && rects[1].y == rects[0].y, "side by side");
1216         assert!(l.height >= l.row_h + 50.0);
1217     }
1218 
1219     #[test]
1220     fn typing_enter_and_list_continuation() {
1221         let mut e = editor("");
1222         for c in ["-", " ", "[", " ", "]", " ", "a"] {
1223             e.key(&typed(c));
1224         }
1225         assert_eq!(e.key(&named(NamedKey::Enter)), Response::Changed);
1226         e.key(&typed("b"));
1227         assert_eq!(e.text(), "- [ ] a\n- [ ] b");
1228         e.key(&named(NamedKey::Enter));
1229         // Enter on an empty item ends the list.
1230         e.key(&named(NamedKey::Enter));
1231         assert_eq!(e.text(), "- [ ] a\n- [ ] b\n");
1232         let mut e = editor("3. three");
1233         e.buf.caret = Pos::new(0, 8);
1234         e.key(&named(NamedKey::Enter));
1235         assert_eq!(e.text(), "3. three\n4. ");
1236     }
1237 
1238     #[test]
1239     fn tab_indents_list_items_and_undo_works() {
1240         let mut e = editor("- a\n- b");
1241         e.buf.caret = Pos::new(1, 3);
1242         e.key(&named(NamedKey::Tab));
1243         assert_eq!(e.text(), "- a\n\t- b");
1244         assert_eq!(e.buf.caret, Pos::new(1, 4));
1245         e.key(&key(Key::Named(NamedKey::Tab), None, false, true));
1246         assert_eq!(e.text(), "- a\n- b");
1247         assert!(e.undo());
1248         assert_eq!(e.text(), "- a\n\t- b");
1249     }
1250 
1251     #[test]
1252     fn arrows_words_home_end_and_vertical() {
1253         let mut e = editor("- [ ] first item\nsecond");
1254         e.buf.caret = Pos::new(0, 10);
1255         e.key(&named(NamedKey::Home));
1256         assert_eq!(e.buf.caret, Pos::new(0, 6), "smart home: the content start");
1257         e.key(&named(NamedKey::Home));
1258         assert_eq!(e.buf.caret, Pos::new(0, 0));
1259         e.key(&named(NamedKey::End));
1260         assert_eq!(e.buf.caret, Pos::new(0, 16));
1261         e.key(&named(NamedKey::ArrowDown));
1262         assert_eq!(e.buf.caret.line, 1);
1263         e.key(&key(Key::Named(NamedKey::ArrowLeft), None, true, true));
1264         assert_eq!(e.buf.selected_text().as_deref(), Some("second"));
1265     }
1266 
1267     #[test]
1268     fn clicks_place_the_caret_and_toggle_tasks() {
1269         let mut e = editor("- [ ] todo\nplain line");
1270         // Line 0 is not active (the caret is on it? it starts at 0,0 — move it).
1271         e.buf.caret = Pos::new(1, 0);
1272         let mut pc = PaintCtx::new();
1273         e.paint(&mut pc, Rect { x: 0.0, y: 0.0, width: 600.0, height: 400.0 }, true);
1274         let l = e.layouts[0].as_ref().unwrap();
1275         let (r, _) = l.task.unwrap();
1276         let (sx, sy) = (e.origin.0 + r.x + r.width / 2.0, e.origin.1 + r.y + r.height / 2.0);
1277         assert_eq!(e.press(sx, sy, false, false), Response::Changed);
1278         assert_eq!(e.text(), "- [x] todo\nplain line");
1279         assert_eq!(e.buf.caret, Pos::new(1, 0), "ticking does not move the caret");
1280         // A click at the far left of the second line puts the caret there.
1281         let y1 = e.origin.1 + e.tops[1] + 3.0;
1282         e.release();
1283         e.press(e.origin.0 + 1.0, y1, false, false);
1284         assert_eq!(e.buf.caret, Pos::new(1, 0));
1285     }
1286 
1287     #[test]
1288     fn rendered_links_follow() {
1289         let mut e = editor("see [[Target|it]] now\nsecond");
1290         e.buf.caret = Pos::new(1, 0);
1291         let mut pc = PaintCtx::new();
1292         e.paint(&mut pc, Rect { x: 0.0, y: 0.0, width: 600.0, height: 400.0 }, true);
1293         let l = e.layouts[0].as_ref().unwrap();
1294         let run = l.runs.iter().find(|r| r.link.is_some()).unwrap();
1295         let (sx, sy) = (e.origin.0 + run.x + 2.0, e.origin.1 + 4.0);
1296         assert_eq!(e.press(sx, sy, false, false), Response::Follow(Target::Note { target: "Target".into(), subpath: None }));
1297     }
1298 
1299     #[test]
1300     fn the_frontmatter_is_a_table_until_the_caret_enters_it() {
1301         let mut e = editor("---\ndone: false\ntags: [x]\n---\nbody");
1302         e.buf.caret = Pos::new(4, 0);
1303         let mut pc = PaintCtx::new();
1304         e.paint(&mut pc, Rect { x: 0.0, y: 0.0, width: 600.0, height: 400.0 }, true);
1305         let header = e.layouts[0].as_ref().unwrap();
1306         assert!(matches!(&header.decos[0], Deco::Text { text, .. } if text == "Properties"));
1307         let row = e.layouts[1].as_ref().unwrap();
1308         let (r, _, on) = row.toggle.clone().expect("a checkbox");
1309         assert!(!on);
1310         // Ticking the box writes `true`, and the caret stays put.
1311         let (sx, sy) = (e.origin.0 + r.x + r.width / 2.0, e.origin.1 + e.tops[1] + r.y + r.height / 2.0);
1312         assert_eq!(e.press(sx, sy, false, false), Response::Changed);
1313         assert_eq!(e.text(), "---\ndone: true\ntags: [x]\n---\nbody");
1314         assert_eq!(e.buf.caret, Pos::new(4, 0));
1315         e.release();
1316         // The pill is padded inside the value column.
1317         e.paint(&mut pc, Rect { x: 0.0, y: 0.0, width: 600.0, height: 400.0 }, true);
1318         let tags = e.layouts[2].as_ref().unwrap();
1319         assert_eq!(tags.runs.len(), 1);
1320         assert!(tags.runs[0].look.pill && tags.runs[0].x >= tags.content_x + layout::PILL_PAD - 0.01);
1321         // The caret in the block shows all of it raw.
1322         e.buf.set_caret(Pos::new(2, 0), false);
1323         e.paint(&mut pc, Rect { x: 0.0, y: 0.0, width: 600.0, height: 400.0 }, true);
1324         for i in 0..4 {
1325             let l = e.layouts[i].as_ref().unwrap();
1326             assert_eq!(l.runs.iter().map(|r| r.text.as_str()).collect::<String>(), e.buf.line(i), "line {i} raw");
1327         }
1328     }
1329 
1330     #[test]
1331     fn a_long_document_shapes_only_what_shows() {
1332         let text: String = (0..5000).map(|i| format!("line {i} with **some** text\n")).collect();
1333         let e = editor(&text);
1334         let laid = e.layouts.iter().filter(|l| l.is_some()).count();
1335         assert!(laid < 60, "{laid} lines shaped");
1336         assert!(e.content_height() > 5000.0 * 14.0);
1337     }
1338 }