Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor 17f9f40ede Notes: place bullets in one pass over the galley rows 2026-11-04 03:59:20 +00:00
2 changed files with 39 additions and 24 deletions
+3 -20
View File
@@ -304,12 +304,7 @@ impl Dictionary {
/// `w` made of dictionary parts allowed in compounds (`COMPOUNDFLAG`, or the begin, middle and
/// end flags), each at least `COMPOUNDMIN` characters long.
fn compounded(&self, w: &[char], index: usize, failed: &mut HashSet<(usize, usize)>) -> bool {
// `w` is always a tail of the checked word, so its length names it: a (tail, part index)
// that failed once fails again however the earlier parts were split.
if failed.contains(&(w.len(), index)) {
return false;
}
fn compounded(&self, w: &[char], index: usize) -> bool {
if self.compound.is_none() && self.compound_end.is_none() {
return false;
}
@@ -327,11 +322,10 @@ impl Dictionary {
if [self.compound, self.compound_end].into_iter().flatten().any(|f| self.compound_part(rest, f, true)) {
return true;
}
if self.compounded(rest, index + 1, failed) {
if self.compounded(rest, index + 1) {
return true;
}
}
failed.insert((w.len(), index));
false
}
@@ -358,7 +352,7 @@ impl Dictionary {
return true;
}
}
self.compounded(&w, 0, &mut HashSet::new())
self.compounded(&w, 0)
}
/// Is `word` spelt correctly? Capitalised and upper-case forms of a lower-case word pass.
@@ -937,17 +931,6 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
}
}
#[test]
fn overlapping_compound_roots_with_a_failing_ending_stay_cheap() {
// Roots a..aaaaaaaa overlap, so many splits of "aaa…ab" reach the same (tail, part) state; unmemoized that is millions of searches.
let d = Dictionary::parse(
b"COMPOUNDFLAG X\nCOMPOUNDMIN 1\n".as_slice(),
b"8\na/X\naa/X\naaa/X\naaaa/X\naaaaa/X\naaaaaa/X\naaaaaaa/X\naaaaaaaa/X\n".as_slice(),
);
assert!(!d.check(&format!("{}b", "a".repeat(99))));
assert!(d.check(&"a".repeat(20)));
}
#[test]
fn runs_give_words_their_language() {
use deckcraft_model::{Paragraph, Run, RunProps};
+36 -4
View File
@@ -8,7 +8,7 @@ use std::sync::Arc;
use deckcraft_model::edit;
use deckcraft_model::text::{Bullet, Paragraph, RunProps, Strike, TextBody};
use egui::text::{CCursor, Galley, LayoutJob, TextFormat};
use egui::text::{Galley, LayoutJob, TextFormat};
use egui::{Align2, Color32, Stroke, Ui, vec2};
use crate::theme;
@@ -210,16 +210,30 @@ pub fn editor(
out
}
/// The (min y, max y) of the row holding char index `at`; `walk` (row index, chars through that
/// row) carries over between calls, so successive non-decreasing `at`s cost one pass in all.
fn row_span_at(galley: &Galley, walk: &mut (usize, usize), at: usize) -> (f32, f32) {
while walk.1 <= at && walk.0 + 1 < galley.rows.len() {
walk.0 += 1;
walk.1 += galley.rows[walk.0].char_count_including_newline().0;
}
galley.rows.get(walk.0).map_or((0.0, 0.0), |r| (r.min_y(), r.max_y()))
}
/// Bullets and numbers at the start of each paragraph's first line of `galley` (laid out from
/// `body`), drawn at `origin`.
fn paint_bullets(painter: &egui::Painter, body: &TextBody, galley: &Galley, origin: egui::Pos2, size: f32, color: Color32) {
let k = size / 12.0;
let mut at = 0usize;
// Paragraph starts only grow, so one forward walk over the rows finds each start's row
// (`pos_from_cursor` would rescan from the top for every bullet).
let mut walk = (0usize, galley.rows.first().map_or(0, |r| r.char_count_including_newline().0));
for (p, bullet) in body.paragraphs.iter().zip(bullets(body)) {
if let Some(b) = bullet {
let row = galley.pos_from_cursor(CCursor::new(at));
let pos = origin + vec2(px(indents(p).0, k), row.center().y);
let font = theme::font(row.height().clamp(size * 0.75, size * 2.0) * 0.8);
let (min_y, max_y) = row_span_at(galley, &mut walk, at);
let (center_y, height) = ((min_y + max_y) / 2.0, max_y - min_y);
let pos = origin + vec2(px(indents(p).0, k), center_y);
let font = theme::font(height.clamp(size * 0.75, size * 2.0) * 0.8);
painter.text(pos, Align2::LEFT_CENTER, b, font, color);
}
at += p.char_len() + 1;
@@ -241,6 +255,7 @@ mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use deckcraft_model::text::{ParaProps, Run};
use egui::text::CCursor;
use super::*;
@@ -317,6 +332,23 @@ mod tests {
assert!(wrapped.iter().all(|r| (r.pos.x - 22.5).abs() < 1.0), "wrapped lines align with the text");
}
#[test]
fn row_walk_matches_cursor_lookup_at_paragraph_starts() {
let ctx = egui::Context::default();
let body = notes();
let mut laid = None;
let mut out = ctx.run_ui(Default::default(), |ui| laid = Some(galley(ui, &body, 12.0, Color32::BLACK, 300.0)));
out.textures_delta.clear();
let g = laid.unwrap();
let mut walk = (0usize, g.rows.first().map_or(0, |r| r.char_count_including_newline().0));
let mut at = 0usize;
for p in &body.paragraphs {
let want = g.pos_from_cursor(CCursor::new(at));
assert_eq!(row_span_at(&g, &mut walk, at), (want.min.y, want.max.y), "paragraph at char {at}");
at += p.char_len() + 1;
}
}
#[test]
fn typing_keeps_the_formatting_around_it() {
let mut d = Draft::new(&notes());