1#![deny(
20 clippy::unwrap_used,
21 clippy::expect_used,
22 clippy::panic,
23 clippy::unimplemented,
24 clippy::todo,
25 clippy::unreachable
26)]
27#![forbid(unsafe_code)]
28
29mod bidi;
30pub mod datetime;
31
32use std::collections::HashMap;
33use std::sync::Arc;
34
35use deckcraft_color::Rgba;
36use deckcraft_fonts::script::{self, joins_following, joins_preceding};
37use deckcraft_fonts::{FontDb, FontFace, FontSlot, ScriptTag, ShapeParams};
38use deckcraft_geom::{Point, Rect};
39use deckcraft_model::resolve::{self, Ctx};
40use deckcraft_model::text::{
41 Align, Anchor, AutoFit, BodyProps, Bullet, Caps, ParaProps, RunKind, RunProps, Spacing, Strike,
42 TextBody, TextDir,
43};
44use deckcraft_model::{Fill, Shape};
45use unicode_segmentation::UnicodeSegmentation;
46
47#[derive(Clone)]
49pub struct GlyphRun {
50 pub face: Arc<FontFace>,
51 pub size: f64,
53 pub color: Rgba,
54 pub glyphs: Vec<(u32, f64, f64)>,
56 pub cells: Vec<(usize, char)>,
58 pub fake_bold: bool,
60 pub fake_italic: bool,
62 pub outline: Option<(Rgba, f64)>,
63 pub para: usize,
65 pub chars: (usize, usize),
66 pub link: bool,
67 pub alpha: f64,
69 pub gradient: Option<deckcraft_model::style::Gradient>,
70 pub effects: Option<RunEffects>,
72}
73
74#[derive(Clone, Copy, Debug, PartialEq)]
76pub struct RunEffects {
77 pub shadow: Option<TextShadow>,
78 pub glow: Option<(Rgba, f64)>,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq)]
84pub struct TextShadow {
85 pub color: Rgba,
86 pub blur: f64,
87 pub dx: f64,
88 pub dy: f64,
89}
90
91impl std::fmt::Debug for GlyphRun {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("GlyphRun")
94 .field("family", &self.face.family)
95 .field("size", &self.size)
96 .field("glyphs", &self.glyphs.len())
97 .finish()
98 }
99}
100
101#[derive(Clone, Debug)]
103pub struct Deco {
104 pub rect: Rect,
105 pub color: Rgba,
106 pub behind: bool,
108 pub para: usize,
110}
111
112#[derive(Clone, Debug, Default)]
113pub struct LineInfo {
114 pub para: usize,
115 pub start: usize,
117 pub end: usize,
118 pub top: f64,
119 pub baseline: f64,
120 pub bottom: f64,
121 pub caret_x: Vec<f64>,
125 pub column: usize,
127 pub rtl: bool,
129 pub levels: Vec<u8>,
131 pub edges: Vec<(f64, f64)>,
134 pub boundary: Vec<bool>,
136 pub extent: (f64, f64),
138 pub start_offset: f64,
140}
141
142#[derive(Clone, Debug, Default)]
143pub struct TextLayout {
144 pub runs: Vec<GlyphRun>,
145 pub decos: Vec<Deco>,
146 pub lines: Vec<LineInfo>,
147 pub inner: Rect,
149 pub content_height: f64,
151 pub content_width: f64,
153 pub font_scale: f64,
155 pub line_reduction: f64,
156 pub overflow: bool,
157 pub rotation: f64,
159 pub para_lines: Vec<(usize, usize)>,
161 pub boundaries: Vec<Vec<bool>>,
164}
165
166pub trait Fields {
168 fn field(&self, kind: &str) -> Option<String>;
169 fn field_in(&self, kind: &str, lang: Option<&str>) -> Option<String> {
172 let _ = lang;
173 self.field(kind)
174 }
175}
176
177pub struct NoFields;
178impl Fields for NoFields {
179 fn field(&self, kind: &str) -> Option<String> {
180 if kind == "slidenum" {
181 Some("1".into())
182 } else {
183 None
184 }
185 }
186}
187
188pub struct Opts<'a> {
189 pub rect: Rect,
191 pub fields: &'a dyn Fields,
192 pub prompt_color: Option<Rgba>,
194 pub no_shrink: bool,
196}
197
198struct Style {
199 face: Arc<FontFace>,
200 size: f64,
201 color: Rgba,
202 alpha: f64,
203 fake_bold: bool,
204 fake_italic: bool,
205 baseline: f64,
206 spacing: f64,
207 caps: Caps,
208 underline: Option<Rgba>,
209 strike: Option<Strike>,
210 highlight: Option<Rgba>,
211 outline: Option<(Rgba, f64)>,
212 link: bool,
213 gradient: Option<deckcraft_model::style::Gradient>,
214 effects: Option<RunEffects>,
215 style_name: &'static str,
217 lang: Option<String>,
219 ea: Option<String>,
221 cs: Option<String>,
222 sym: Option<String>,
225 major: bool,
227 kern: bool,
229}
230
231#[derive(Clone)]
233struct Cell {
234 ch: char,
235 style: usize,
236 face: usize,
237 level: u8,
239 glyphs: Vec<(u32, f64, f64)>,
242 adv: f64,
244 head: usize,
246 boundary: bool,
248}
249
250impl Cell {
251 fn breakable(&self, i: usize) -> bool {
253 self.boundary && self.head == i
254 }
255}
256
257struct ParaShaped {
258 cells: Vec<Cell>,
259 styles: Vec<Style>,
260 faces: Vec<Arc<FontFace>>,
261 props: ParaProps,
262 lead_size: f64,
264 lead_face: Arc<FontFace>,
265 bullet: Option<(String, Style)>,
266 #[allow(dead_code)]
267 level: u8,
268 rtl: bool,
270}
271
272fn style_name(bold: bool, italic: bool) -> &'static str {
273 match (bold, italic) {
274 (true, true) => "Bold Italic",
275 (true, false) => "Bold",
276 (false, true) => "Italic",
277 (false, false) => "Regular",
278 }
279}
280
281fn make_style(ctx: &Ctx, r: &RunProps, scale: f64, prompt: Option<Rgba>) -> Style {
282 let family = resolve::font_family(ctx, r);
283 let bold = r.bold.unwrap_or(false);
284 let italic = r.italic.unwrap_or(false);
285 let face = FontDb::global().document_face(&family, style_name(bold, italic));
286 let fake_bold = bold && face.weight < 550.0;
287 let fake_italic = italic && !face.italic;
288 let size = (r.size.unwrap_or(18.0) * scale).clamp(0.5, 4000.0);
289 let (mut color, alpha, gradient) = match &r.fill {
290 Some(Fill::Solid { color }) => (ctx.color(color, None), color.alpha(), None),
291 Some(Fill::Gradient(g)) => {
292 let mut g = g.clone();
294 for s in &mut g.stops {
295 s.color = deckcraft_model::ColorRef::rgb(ctx.color(&s.color, None));
296 }
297 (
298 g.stops
299 .first()
300 .map(|s| ctx.color(&s.color, None))
301 .unwrap_or(Rgba::BLACK),
302 1.0,
303 Some(g),
304 )
305 }
306 Some(Fill::None) => (Rgba::TRANSPARENT, 0.0, None),
307 _ => (resolve::text_color(ctx, r), 1.0, None),
308 };
309 if let Some(p) = prompt {
310 color = p;
311 }
312 let major = r.font.as_deref().is_some_and(|f| f.starts_with("+mj"));
313 let theme = &ctx.master.theme;
314 let slot_family = |explicit: &Option<String>, theme_ref: &str| {
315 let name = theme.font(explicit.as_deref().unwrap_or(theme_ref));
316 (!name.trim().is_empty()).then_some(name)
317 };
318 let ea = slot_family(&r.font_ea, if major { "+mj-ea" } else { "+mn-ea" });
319 let cs = slot_family(&r.font_cs, if major { "+mj-cs" } else { "+mn-cs" });
320 let underline = r.underline.as_deref().filter(|u| *u != "none").map(|_| {
321 r.underline_color
322 .as_ref()
323 .map(|c| ctx.color(c, None))
324 .unwrap_or(color)
325 });
326 let outline = r.outline.as_ref().and_then(|l| match &l.fill {
327 Some(Fill::Solid { color: c }) => Some((ctx.color(c, None), l.width.unwrap_or(0.75))),
328 _ => None,
329 });
330 Style {
331 face,
332 size,
333 color,
334 alpha,
335 fake_bold,
336 fake_italic,
337 baseline: r.baseline.unwrap_or(0.0),
338 spacing: r.spacing.unwrap_or(0.0) * scale,
339 caps: r.caps.unwrap_or(Caps::None),
340 underline,
341 strike: r.strike.filter(|s| *s != Strike::None),
342 highlight: r.highlight.as_ref().map(|c| ctx.color(c, None)),
343 outline,
344 link: r.link.is_some(),
345 gradient,
346 effects: run_effects(ctx, r, scale),
347 style_name: style_name(bold, italic),
348 lang: r.lang.clone().filter(|l| !l.is_empty()),
349 ea,
350 cs,
351 sym: r
352 .font_sym
353 .as_deref()
354 .map(|f| ctx.master.theme.font(f))
355 .filter(|f| !f.trim().is_empty()),
356 major,
357 kern: r.kern.is_some_and(|k| k > 0.0 && size + 1e-6 >= k * scale),
358 }
359}
360
361fn face_chain(
364 ctx: &Ctx,
365 st: &Style,
366 slot: FontSlot,
367 script: Option<ScriptTag>,
368) -> Vec<Arc<FontFace>> {
369 let db = FontDb::global();
370 let mut chain: Vec<Arc<FontFace>> = Vec::with_capacity(3);
371 let mut push = |family: &str| {
372 if db.has_family(family) {
374 let f = db.face(family, st.style_name);
375 if !chain.iter().any(|c| Arc::ptr_eq(c, &f)) {
376 chain.push(f);
377 }
378 }
379 };
380 match slot {
381 FontSlot::EastAsian => st.ea.as_deref().into_iter().for_each(&mut push),
382 FontSlot::ComplexScript => st.cs.as_deref().into_iter().for_each(&mut push),
383 FontSlot::Latin => {}
384 }
385 if let Some(sc) = script.and_then(|t| std::str::from_utf8(&t).ok().map(String::from)) {
386 let set = if st.major {
387 &ctx.master.theme.fonts.major
388 } else {
389 &ctx.master.theme.fonts.minor
390 };
391 if let Some(f) = set.script(&sc) {
392 push(f);
393 }
394 }
395 if !chain.iter().any(|c| Arc::ptr_eq(c, &st.face)) {
396 chain.push(st.face.clone());
397 }
398 chain
399}
400
401fn spacing_pts(s: Option<Spacing>, size: f64, reduce: f64) -> f64 {
402 match s {
403 Some(Spacing::Pct(p)) => p * size * 1.2 * (1.0 - reduce),
404 Some(Spacing::Pts(v)) => v * (1.0 - reduce),
405 None => 0.0,
406 }
407}
408
409fn roman(mut n: u32, upper: bool) -> String {
410 let table = [
411 (1000, "m"),
412 (900, "cm"),
413 (500, "d"),
414 (400, "cd"),
415 (100, "c"),
416 (90, "xc"),
417 (50, "l"),
418 (40, "xl"),
419 (10, "x"),
420 (9, "ix"),
421 (5, "v"),
422 (4, "iv"),
423 (1, "i"),
424 ];
425 let mut s = String::new();
426 if n == 0 || n > 3999 {
427 return n.to_string();
428 }
429 for (v, r) in table {
430 while n >= v {
431 s.push_str(r);
432 n -= v;
433 }
434 }
435 if upper { s.to_uppercase() } else { s }
436}
437
438fn alpha_num(n: u32, upper: bool) -> String {
439 if n == 0 {
440 return String::new();
441 }
442 let mut n = n - 1;
443 let c = (b'a' + (n % 26) as u8) as char;
444 let reps = n / 26 + 1;
445 n = reps;
446 let s: String = std::iter::repeat_n(c, n as usize).collect();
447 if upper { s.to_uppercase() } else { s }
448}
449
450pub fn autonum(scheme: &str, n: u32) -> String {
452 let (core, upper) = if scheme.starts_with("romanUc") {
453 (roman(n, true), true)
454 } else if scheme.starts_with("romanLc") {
455 (roman(n, false), false)
456 } else if scheme.starts_with("alphaUc") {
457 (alpha_num(n, true), true)
458 } else if scheme.starts_with("alphaLc") {
459 (alpha_num(n, false), false)
460 } else if scheme.starts_with("circleNum") {
461 let c = char::from_u32(0x2460 + n.saturating_sub(1).min(19)).unwrap_or('•');
462 return c.to_string();
463 } else {
464 (n.to_string(), false)
465 };
466 let _ = upper;
467 if scheme.ends_with("ParenBoth") {
468 format!("({core})")
469 } else if scheme.ends_with("ParenR") {
470 format!("{core})")
471 } else if scheme.ends_with("Period") {
472 format!("{core}.")
473 } else if scheme.ends_with("Minus") {
474 format!("- {core} -")
475 } else {
476 core
477 }
478}
479
480fn shape_para(
481 ctx: &Ctx,
482 shape: &Shape,
483 body: &TextBody,
484 pi: usize,
485 scale: f64,
486 opts: &Opts,
487 number: Option<u32>,
488) -> ParaShaped {
489 let db = FontDb::global();
490 let Some(para) = body.paragraphs.get(pi) else {
491 let face = db.face("Inter", "Regular");
492 return ParaShaped {
493 cells: vec![],
494 styles: vec![],
495 faces: vec![],
496 props: ParaProps::default(),
497 lead_size: 18.0,
498 lead_face: face,
499 bullet: None,
500 level: 0,
501 rtl: false,
502 };
503 };
504 let props = resolve::para(ctx, shape, para);
505 let rtl = props.rtl.unwrap_or(false);
506 let mut styles: Vec<Style> = vec![];
507 let mut faces: Vec<Arc<FontFace>> = vec![];
508 let mut text = String::new();
510 let mut char_style: Vec<usize> = vec![];
511 for run in ¶.runs {
512 let rp = resolve::run(ctx, shape, para, &run.props);
513 let st = make_style(ctx, &rp, scale, opts.prompt_color);
514 let t: String = match &run.kind {
515 RunKind::Text => run.text.clone(),
516 RunKind::Break => "\u{b}".into(),
517 RunKind::Field { field } => opts
518 .fields
519 .field_in(field, rp.lang.as_deref())
520 .unwrap_or_else(|| run.text.clone()),
521 RunKind::Math { .. } => run.text.clone(),
522 };
523 let si = styles.len();
524 styles.push(st);
525 char_style.extend(std::iter::repeat_n(si, t.chars().count()));
526 text.push_str(&t);
527 }
528 let cells = shape_cells(ctx, &text, &char_style, &styles, &mut faces, rtl);
529 let lead = para
530 .runs
531 .first()
532 .map(|r| resolve::run(ctx, shape, para, &r.props))
533 .unwrap_or_else(|| resolve::run(ctx, shape, para, ¶.end_props));
534 let lead_style = make_style(ctx, &lead, scale, opts.prompt_color);
535 let lead_size = lead_style.size;
536 let lead_face = lead_style.face.clone();
537 let bullet = if para.is_empty() && opts.prompt_color.is_none() {
539 None
540 } else {
541 match &props.bullet {
542 Some(Bullet::Char { char }) => {
543 let mut rp = lead.clone();
544 if let Some(f) = &props.bullet_font {
545 rp.font = Some(f.clone());
546 }
547 rp.bold = Some(false);
548 rp.italic = Some(false);
549 if let Some(c) = &props.bullet_color {
550 rp.fill = Some(Fill::solid(c.clone()));
551 }
552 rp.size = Some(lead.size.unwrap_or(18.0) * props.bullet_size.unwrap_or(1.0));
553 rp.underline = None;
554 rp.strike = None;
555 rp.highlight = None;
556 let mut st = make_style(ctx, &rp, scale, opts.prompt_color);
557 st.baseline = 0.0;
558 let ch = map_symbol_bullet(char, props.bullet_font.as_deref());
560 bullet_face(db, &mut st, &ch);
561
562 let ch = match symbol_font_char(&st.face, char, props.bullet_font.as_deref()) {
566 Some(c) => c.to_string(),
567 None => map_symbol_bullet(char, props.bullet_font.as_deref()),
568 };
569 if !st.face.covers(ch.chars().next().unwrap_or('•')) {
570 st.face = db.face("Inter", "Regular");
571 }
572 Some((ch, st))
573 }
574 Some(Bullet::AutoNum { scheme, start_at }) => {
575 let mut rp = lead.clone();
576 if let Some(c) = &props.bullet_color {
577 rp.fill = Some(Fill::solid(c.clone()));
578 }
579 rp.size = Some(lead.size.unwrap_or(18.0) * props.bullet_size.unwrap_or(1.0));
580 rp.underline = None;
581 let mut st = make_style(ctx, &rp, scale, opts.prompt_color);
582 let label = autonum(
583 scheme,
584 start_at
585 .saturating_sub(1)
586 .saturating_add(number.unwrap_or(1)),
587 );
588 bullet_face(db, &mut st, &label);
589 Some((label, st))
590 }
591 _ => None,
592 }
593 };
594 ParaShaped {
595 cells,
596 styles,
597 faces,
598 props,
599 lead_size,
600 lead_face,
601 bullet,
602 level: para.level,
603 rtl,
604 }
605}
606
607fn bullet_face(db: &FontDb, st: &mut Style, label: &str) {
610 let chars: Vec<char> = label.chars().collect();
611 if !chars.is_empty() && !deckcraft_fonts::covers_cluster(&st.face, &chars) {
612 st.face = db.cascade(&chars, std::slice::from_ref(&st.face), st.style_name);
613 }
614}
615
616fn run_effects(ctx: &Ctx, r: &RunProps, scale: f64) -> Option<RunEffects> {
620 let shadow = r.shadow.as_ref().filter(|s| !s.inner).map(|s| {
621 let a = s.dir.to_radians();
622 TextShadow {
623 color: ctx.color(&s.color, None),
624 blur: s.blur.max(0.0) * scale,
625 dx: a.cos() * s.dist * scale,
626 dy: a.sin() * s.dist * scale,
627 }
628 });
629 let glow = r
630 .glow
631 .as_ref()
632 .filter(|g| g.radius > 0.0)
633 .map(|g| (ctx.color(&g.color, None), g.radius * scale));
634 (shadow.is_some() || glow.is_some()).then_some(RunEffects { shadow, glow })
635}
636
637fn shape_cells(
638 ctx: &Ctx,
639 text: &str,
640 char_style: &[usize],
641 styles: &[Style],
642 faces: &mut Vec<Arc<FontFace>>,
643 rtl: bool,
644) -> Vec<Cell> {
645 let db = FontDb::global();
646 let chars: Vec<char> = text.chars().collect();
647 let n = chars.len();
648 let mut byte_of: Vec<usize> = text.char_indices().map(|(b, _)| b).collect();
650 byte_of.push(text.len());
651 let char_at_byte: HashMap<usize, usize> =
652 byte_of.iter().enumerate().map(|(i, b)| (*b, i)).collect();
653 let levels = bidi::levels(text, &chars, rtl);
654 let scripts = script::resolve(&chars);
655 let mut boundary = vec![false; n];
656 let mut graphemes: Vec<(usize, usize)> = Vec::new();
657 for (b, g) in text.grapheme_indices(true) {
658 let Some(&i) = char_at_byte.get(&b) else {
659 continue;
660 };
661 if let Some(x) = boundary.get_mut(i) {
662 *x = true;
663 }
664 graphemes.push((i, i + g.chars().count()));
665 }
666 let face_index = |faces: &mut Vec<Arc<FontFace>>, f: &Arc<FontFace>| -> usize {
667 if let Some(i) = faces.iter().position(|x| Arc::ptr_eq(x, f)) {
668 return i;
669 }
670 faces.push(f.clone());
671 faces.len() - 1
672 };
673 let mut char_face = vec![0usize; n];
675 let mut chains: HashMap<(usize, FontSlot, Option<ScriptTag>), Vec<Arc<FontFace>>> =
676 HashMap::new();
677 let mut prev: Option<Arc<FontFace>> = None;
678 for &(a, b) in &graphemes {
679 let Some(cluster) = chars.get(a..b) else {
680 continue;
681 };
682 let si = char_style.get(a).copied().unwrap_or(0);
683 let Some(st) = styles.get(si) else { continue };
684 let (sc, slot) = scripts.get(a).copied().unwrap_or((None, FontSlot::Latin));
685 let neutral = cluster.iter().all(|c| script::script_of(*c).is_none())
689 && !deckcraft_fonts::is_emoji_cluster(cluster)
690 && !cluster.contains(&'\u{FE0E}');
691 let sym_face = st
693 .sym
694 .as_deref()
695 .filter(|_| {
696 cluster
697 .first()
698 .is_some_and(|c| ('\u{F000}'..='\u{F0FF}').contains(c))
699 })
700 .filter(|f| db.has_family(f))
701 .map(|f| db.face(f, st.style_name))
702 .filter(|f| deckcraft_fonts::covers_cluster(f, cluster));
703 let face = match (sym_face, prev.as_ref()) {
704 (Some(f), _) => f,
705 (None, Some(p))
706 if neutral
707 && deckcraft_fonts::covers_cluster(p, cluster)
708 && char_style.get(a.wrapping_sub(1)) == Some(&si) =>
709 {
710 p.clone()
711 }
712 (None, _) => {
713 let chain = chains
714 .entry((si, slot, sc))
715 .or_insert_with(|| face_chain(ctx, st, slot, sc));
716 db.cascade(cluster, chain, st.style_name)
717 }
718 };
719 let fi = face_index(faces, &face);
720 for f in char_face.get_mut(a..b).into_iter().flatten() {
721 *f = fi;
722 }
723 prev = Some(face);
724 }
725 let mut cells: Vec<Cell> = (0..n)
726 .map(|i| Cell {
727 ch: chars.get(i).copied().unwrap_or(' '),
728 style: char_style.get(i).copied().unwrap_or(0),
729 face: char_face.get(i).copied().unwrap_or(0),
730 level: levels.get(i).copied().unwrap_or(0),
731 glyphs: vec![],
732 adv: 0.0,
733 head: i,
734 boundary: boundary.get(i).copied().unwrap_or(true),
735 })
736 .collect();
737 let upper = |i: usize| {
739 styles
740 .get(char_style.get(i).copied().unwrap_or(0))
741 .is_some_and(|st| st.caps != Caps::None)
742 };
743 let lang = |i: usize| {
744 styles
745 .get(char_style.get(i).copied().unwrap_or(0))
746 .and_then(|st| st.lang.as_deref())
747 };
748 let kern = |i: usize| {
749 styles
750 .get(char_style.get(i).copied().unwrap_or(0))
751 .is_some_and(|st| st.kern)
752 };
753 let key = |i: usize| {
754 (
755 levels.get(i).copied(),
756 char_face.get(i).copied(),
757 scripts.get(i).map(|s| s.0),
758 upper(i),
759 lang(i),
760 kern(i),
761 )
762 };
763 let mut a = 0;
764 while a < n {
765 let mut b = a + 1;
766 while b < n && key(b) == key(a) {
767 b += 1;
768 }
769 shape_item(
770 &mut cells,
771 styles,
772 faces,
773 text,
774 &byte_of,
775 &char_at_byte,
776 a..b,
777 scripts.get(a).and_then(|s| s.0),
778 lang(a),
779 upper(a),
780 );
781 a = b;
782 }
783 cells
784}
785
786const SUB_SUPER: f64 = 0.66;
788
789fn shaping_features(
794 cells: &[Cell],
795 styles: &[Style],
796 script: Option<ScriptTag>,
797) -> Vec<deckcraft_fonts::Feature> {
798 let mut f = vec![];
799 if !cells
800 .first()
801 .and_then(|c| styles.get(c.style))
802 .is_some_and(|st| st.kern)
803 {
804 f.extend(deckcraft_fonts::feature("-kern"));
805 }
806 let alphabetic = matches!(script.as_ref(), Some(b"Latn" | b"Grek" | b"Cyrl"));
807 let emoji = cells.iter().any(|c| matches!(c.ch as u32, 0x200D | 0x20E3 | 0xFE0F | 0x2600..=0x27BF | 0x1F000..=0x1FAFF | 0xE0020..=0xE007F));
808 if alphabetic && !emoji {
809 f.extend(deckcraft_fonts::feature("-liga"));
810 f.extend(deckcraft_fonts::feature("-clig"));
811 }
812 f
813}
814
815#[allow(clippy::too_many_arguments)]
818fn shape_item(
819 cells: &mut [Cell],
820 styles: &[Style],
821 faces: &[Arc<FontFace>],
822 text: &str,
823 byte_of: &[usize],
824 char_at_byte: &HashMap<usize, usize>,
825 range: std::ops::Range<usize>,
826 script: Option<ScriptTag>,
827 lang: Option<&str>,
828 upper: bool,
829) {
830 let Some(first) = cells.get(range.start) else {
831 return;
832 };
833 let (fi, level) = (first.face, first.level);
834 let Some(face) = faces.get(fi) else { return };
835 let rtl = level % 2 == 1;
836 let (Some(&b0), Some(&b1)) = (byte_of.get(range.start), byte_of.get(range.end)) else {
837 return;
838 };
839 let features = shaping_features(cells.get(range.clone()).unwrap_or(&[]), styles, script);
840 let params = ShapeParams {
841 features: &features,
842 rtl,
843 script,
844 language: lang,
845 };
846 let glyphs = deckcraft_fonts::shape_range(face, text, b0..b1, ¶ms, |ch| {
847 if upper {
848 ch.to_uppercase().next().unwrap_or(ch)
849 } else {
850 ch
851 }
852 });
853 let upem = face.upem.max(1.0);
854 let scale_of = |cell: &Cell| {
855 let st = styles.get(cell.style);
856 let size = st.map(|s| s.size).unwrap_or(18.0);
857 let small = st.is_some_and(|s| s.caps == Caps::Small) && cell.ch.is_lowercase();
858 let size = if small { size * 0.8 } else { size };
859 let size = if st.is_some_and(|s| s.baseline != 0.0) {
862 size * SUB_SUPER
863 } else {
864 size
865 };
866 size / upem
867 };
868 let mut heads: Vec<usize> = glyphs
870 .iter()
871 .filter_map(|g| char_at_byte.get(&g.cluster).copied())
872 .filter(|h| range.contains(h))
873 .collect();
874 heads.sort_unstable();
875 heads.dedup();
876 let mut width: HashMap<usize, f64> = HashMap::new();
877 let mut lig: HashMap<usize, (u32, usize)> = HashMap::new();
878 for g in &glyphs {
879 let Some(&h) = char_at_byte.get(&g.cluster) else {
880 continue;
881 };
882 let Some(cell) = cells.get_mut(h) else {
883 continue;
884 };
885 if matches!(cell.ch, '\t' | '\u{b}' | '\n') {
886 continue;
887 }
888 let s = scale_of(cell);
889 let w = width.entry(h).or_insert(0.0);
890 cell.glyphs
891 .push((g.gid, *w + g.x_offset as f64 * s, -(g.y_offset as f64) * s));
892 *w += g.x_advance as f64 * s;
893 let e = lig.entry(h).or_insert((g.gid, 0));
894 e.1 += 1;
895 }
896 for (hi, &h) in heads.iter().enumerate() {
897 let end = heads
898 .get(hi + 1)
899 .copied()
900 .unwrap_or(range.end)
901 .min(range.end);
902 let w = width.get(&h).copied().unwrap_or(0.0);
903 for c in cells.get_mut(h..end).into_iter().flatten() {
904 c.head = h;
905 }
906 let stops: Vec<usize> = (h..end)
907 .filter(|&i| i == h || cells.get(i).is_some_and(|c| c.boundary))
908 .collect();
909 let m = stops.len().max(1);
910 let mut bounds = vec![0.0];
912 if m > 1
913 && let Some(&(gid, 1)) = lig.get(&h)
914 {
915 let s = cells.get(h).map(scale_of).unwrap_or(0.0);
916 let carets = deckcraft_fonts::ligature_carets(face, gid);
917 if carets.len() + 1 >= m {
918 bounds.extend(
919 carets
920 .iter()
921 .take(m - 1)
922 .map(|c| (*c as f64 * s).clamp(0.0, w)),
923 );
924 }
925 }
926 if bounds.len() != m {
927 bounds = (0..m).map(|j| w * j as f64 / m as f64).collect();
928 }
929 bounds.push(w);
930 for (j, &i) in stops.iter().enumerate() {
931 let vj = if rtl { m - 1 - j } else { j };
932 let share =
933 bounds.get(vj + 1).copied().unwrap_or(w) - bounds.get(vj).copied().unwrap_or(0.0);
934 if let Some(c) = cells.get_mut(i) {
935 c.adv = share.max(0.0);
936 }
937 }
938 }
939 for i in range {
941 let Some(c) = cells.get(i) else { continue };
942 let drawn = cells.get(c.head).is_some_and(|hd| !hd.glyphs.is_empty());
943 if c.boundary
944 && (drawn || c.ch == ' ')
945 && let Some(sp) = styles.get(c.style).map(|s| s.spacing)
946 && let Some(c) = cells.get_mut(i)
947 {
948 c.adv += sp;
949 }
950 }
951}
952
953fn is_symbol_font(family: &str) -> bool {
955 let f = family.to_ascii_lowercase();
956 f.starts_with("wingdings")
957 || f == "symbol"
958 || f.starts_with("webdings")
959 || f.starts_with("marlett")
960}
961
962fn symbol_font_char(face: &FontFace, c: &str, font: Option<&str>) -> Option<char> {
965 let font = font?;
966 if !is_symbol_font(font) || !face.family.eq_ignore_ascii_case(font) {
967 return None;
968 }
969 let first = c.chars().next()?;
970 let pua = char::from_u32(0xF000 | ((first as u32) & 0xFF))?;
971 if face.covers(pua) {
972 Some(pua)
973 } else if face.covers(first) {
974 Some(first)
975 } else {
976 None
977 }
978}
979
980pub fn symbol_char(face: &FontFace, font: &str, c: char) -> Option<char> {
985 let code = c as u32;
986 if !is_symbol_font(font) || !(code < 0x100 || (0xF000..=0xF0FF).contains(&code)) {
987 return None;
988 }
989 let plain = char::from_u32(code & 0xFF)?;
990 let s = plain.to_string();
991 Some(symbol_font_char(face, &s, Some(font)).unwrap_or_else(|| {
992 map_symbol_bullet(&s, Some(font))
993 .chars()
994 .next()
995 .unwrap_or('•')
996 }))
997}
998
999fn map_symbol_bullet(c: &str, font: Option<&str>) -> String {
1000 let f = font.unwrap_or("").to_ascii_lowercase();
1001 let first = c.chars().next().unwrap_or('•');
1002 if f.starts_with("webdings") {
1003 let m = match (first as u32) & 0xff {
1004 0x6e => '●',
1005 0x67 => '■',
1006 0x61 => '✓',
1007 0x72 => '✕',
1008 0x33 => '◀',
1009 0x34 => '▶',
1010 0x35 => '▲',
1011 0x36 => '▼',
1012 _ => '•',
1013 };
1014 return m.to_string();
1015 }
1016 if f.contains("wingdings") || f.contains("symbol") || ('\u{f000}'..='\u{f0ff}').contains(&first)
1017 {
1018 let code = (first as u32) & 0xff;
1019 if f.starts_with("wingdings 2") {
1021 let m = match code {
1022 0x97 | 0x98 => '●',
1023 0x9e..=0xa1 => '■',
1024 0xa2..=0xa5 => '◆',
1025 0xea..=0xf3 => '★',
1026 0xb7..=0xbf => '○',
1027 0x50 => '✓',
1028 _ => '•',
1029 };
1030 return m.to_string();
1031 }
1032 if f.starts_with("wingdings 3") {
1033 let m = match code {
1034 0x7d | 0x75 => '►',
1035 0x7c | 0x74 => '◄',
1036 0x70 => '▲',
1037 0x71 => '▼',
1038 0xc6 | 0xc7 => '➔',
1039 _ => '▶',
1040 };
1041 return m.to_string();
1042 }
1043 let m = match code {
1044 0xa7 => '▪',
1045 0xd8 => '➢',
1046 0xfc => '✓',
1047 0x76 => '❖',
1048 0x71 => '❑',
1049 0x6e => '■',
1050 0x6c => '●',
1051 0xa8 => '◆',
1052 0xb7 => '•',
1053 0x2d => '–',
1054 _ => '•',
1055 };
1056 return m.to_string();
1057 }
1058 c.graphemes(true).next().unwrap_or("•").to_string()
1060}
1061
1062fn measure_str(face: &FontFace, size: f64, s: &str) -> (f64, Vec<(u32, f64)>) {
1063 let g = deckcraft_fonts::shape(face, s, &[], |c| c);
1064 let k = size / face.upem.max(1.0);
1065 let mut x = 0.0;
1066 let mut out = vec![];
1067 for gl in g {
1068 out.push((gl.gid, x + gl.x_offset as f64 * k));
1069 x += gl.x_advance as f64 * k;
1070 }
1071 (x, out)
1072}
1073
1074const LINE_PITCH: f64 = 1.2;
1082
1083fn line_metrics(face: &FontFace, size: f64) -> (f64, f64) {
1086 let h = size * LINE_PITCH;
1087 (h * face.line_split, h * (1.0 - face.line_split))
1088}
1089
1090fn is_hangul(c: char) -> bool {
1093 matches!(c, '\u{1100}'..='\u{11FF}' | '\u{3130}'..='\u{318F}' | '\u{A960}'..='\u{A97F}' | '\u{AC00}'..='\u{D7FF}')
1094}
1095
1096fn is_hanging_punct(c: char) -> bool {
1098 matches!(
1099 c,
1100 '、' | '。'
1101 | ','
1102 | '.'
1103 | ':'
1104 | ';'
1105 | '!'
1106 | '?'
1107 | ')'
1108 | '」'
1109 | '』'
1110 | '】'
1111 | '〕'
1112 | '〉'
1113 | '》'
1114 | '〙'
1115 | '〛'
1116 | '}'
1117 | ']'
1118 | '」'
1119 | '、'
1120 | '。'
1121 )
1122}
1123
1124fn break_opportunities(text: &str, latin_break: bool) -> (Vec<bool>, Vec<bool>) {
1128 let n = text.chars().count();
1129 let mut allowed = vec![false; n + 1];
1130 let mut mandatory = vec![false; n + 1];
1131 let mut byte_to_char = std::collections::HashMap::new();
1132 for (ci, (b, _)) in text.char_indices().enumerate() {
1133 byte_to_char.insert(b, ci);
1134 }
1135 byte_to_char.insert(text.len(), n);
1136 for (b, op) in unicode_linebreak::linebreaks(text) {
1137 if let Some(&ci) = byte_to_char.get(&b) {
1138 if let Some(a) = allowed.get_mut(ci) {
1139 *a = true;
1140 }
1141 if op == unicode_linebreak::BreakOpportunity::Mandatory
1142 && let Some(m) = mandatory.get_mut(ci)
1143 {
1144 *m = true;
1145 }
1146 }
1147 }
1148 let chars: Vec<char> = text.chars().collect();
1149 for (i, w) in chars.windows(2).enumerate() {
1150 let (prev, next) = (w[0], w[1]);
1151 let keep = if is_hangul(prev) && is_hangul(next) {
1152 false
1153 } else {
1154 latin_break || !matches!(prev, '/' | '\\') || next.is_whitespace()
1155 };
1156 if !keep
1157 && !mandatory.get(i + 1).copied().unwrap_or(false)
1158 && let Some(a) = allowed.get_mut(i + 1)
1159 {
1160 *a = false;
1161 }
1162 }
1163 (allowed, mandatory)
1164}
1165
1166struct RawLine {
1167 start: usize,
1168 end: usize,
1169 next: usize,
1171}
1172
1173fn break_lines(
1174 p: &ParaShaped,
1175 first_w: f64,
1176 rest_w: f64,
1177 wrap: bool,
1178 tab_stops: &[f64],
1179 default_tab: f64,
1180 x0_first: f64,
1181 x0_rest: f64,
1182) -> Vec<RawLine> {
1183 let text: String = p.cells.iter().map(|c| c.ch).collect();
1184 let (allowed, _) = break_opportunities(&text, p.props.latin_line_break.unwrap_or(false));
1185 let hang = p.props.hanging_punct.unwrap_or(false);
1186 let n = p.cells.len();
1187 let mut lines = vec![];
1188 let mut start = 0;
1189 if n == 0 {
1190 return vec![RawLine {
1191 start: 0,
1192 end: 0,
1193 next: 0,
1194 }];
1195 }
1196 while start < n {
1197 let avail = if lines.is_empty() { first_w } else { rest_w };
1198 let x0 = if lines.is_empty() { x0_first } else { x0_rest };
1199 let mut x = 0.0;
1200 let mut last_break: Option<usize> = None;
1201 let mut i = start;
1202 let mut end = n;
1203 let mut next = n;
1204 while i < n {
1205 let c = p.cells.get(i).map(|c| c.ch).unwrap_or(' ');
1206 if c == '\u{b}' || c == '\n' {
1207 end = i;
1208 next = i + 1;
1209 break;
1210 }
1211 let adv = if c == '\t' {
1212 tab_advance(x0 + x, tab_stops, default_tab)
1213 } else {
1214 p.cells.get(i).map(|c| c.adv).unwrap_or(0.0)
1215 };
1216 let breakable = |j: usize| p.cells.get(j).is_none_or(|c| c.breakable(j));
1217 if i > start && allowed.get(i).copied().unwrap_or(false) && breakable(i) {
1218 last_break = Some(i);
1219 }
1220 if wrap
1223 && hang
1224 && x + adv > avail + 0.01
1225 && x <= avail + 0.01
1226 && is_hanging_punct(c)
1227 && i > start
1228 {
1229 end = i + 1;
1230 next = i + 1;
1231 break;
1232 }
1233 if wrap && x + adv > avail + 0.01 && !c.is_whitespace() && i > start {
1234 let b = last_break.filter(|b| *b > start).unwrap_or_else(|| {
1236 (start + 1..=i)
1237 .rev()
1238 .find(|&j| breakable(j))
1239 .or_else(|| (i + 1..n).find(|&j| breakable(j)))
1240 .unwrap_or(n)
1241 });
1242 end = b;
1243 next = b;
1244 while end > start && p.cells.get(end - 1).is_some_and(|c| c.ch == ' ') {
1246 end -= 1;
1247 }
1248 break;
1249 }
1250 x += adv;
1251 i += 1;
1252 }
1253 if i >= n {
1254 end = n;
1255 next = n;
1256 }
1257 lines.push(RawLine { start, end, next });
1258 if next <= start {
1259 let last = lines.len() - 1;
1261 let to = (start + 1..n)
1262 .find(|&j| p.cells.get(j).is_none_or(|c| c.breakable(j)))
1263 .unwrap_or(n);
1264 if let Some(l) = lines.get_mut(last) {
1265 l.end = to;
1266 l.next = to;
1267 }
1268 start = to;
1269 } else {
1270 start = next;
1271 }
1272 if next == n && p.cells.last().is_some_and(|c| c.ch == '\u{b}') && start >= n {
1273 lines.push(RawLine {
1275 start: n,
1276 end: n,
1277 next: n,
1278 });
1279 }
1280 }
1281 lines
1282}
1283
1284fn tab_advance(x: f64, stops: &[f64], default_tab: f64) -> f64 {
1285 for s in stops {
1286 if *s > x + 0.5 {
1287 return s - x;
1288 }
1289 }
1290 let d = if default_tab > 1.0 { default_tab } else { 72.0 };
1291 let next = ((x / d).floor() + 1.0) * d;
1292 (next - x).max(1.0)
1293}
1294
1295pub fn layout(ctx: &Ctx, shape: &Shape, body: &TextBody, opts: &Opts) -> TextLayout {
1297 let bp = resolve::body(ctx, shape);
1298 let autofit = bp.autofit.unwrap_or(AutoFit::None);
1299 let mut l = layout_scaled(ctx, shape, body, &bp, opts, 1.0, 0.0);
1300 if let (AutoFit::Shrink { .. }, false) = (autofit, opts.no_shrink)
1301 && l.overflow
1302 {
1303 let (mut lo, mut hi) = (0.1_f64, 1.0_f64);
1305 let mut best = None;
1306 for _ in 0..12 {
1307 let mid = (lo + hi) / 2.0;
1308 let red: f64 = ((1.0 - mid) * 0.8_f64).min(0.2);
1309 let t = layout_scaled(ctx, shape, body, &bp, opts, mid, red);
1310 if t.overflow {
1311 hi = mid;
1312 } else {
1313 lo = mid;
1314 best = Some(t);
1315 }
1316 }
1317 l = best.unwrap_or_else(|| layout_scaled(ctx, shape, body, &bp, opts, 0.1, 0.2));
1318 }
1319 l
1320}
1321
1322fn layout_scaled(
1323 ctx: &Ctx,
1324 shape: &Shape,
1325 body: &TextBody,
1326 bp: &BodyProps,
1327 opts: &Opts,
1328 scale: f64,
1329 reduce: f64,
1330) -> TextLayout {
1331 let r = opts.rect;
1332 let vert = bp.vert.unwrap_or(TextDir::Horizontal);
1333 let rotation = match vert {
1334 TextDir::Vertical | TextDir::EaVertical | TextDir::Stacked => 90.0,
1335 TextDir::Vertical270 => 270.0,
1336 TextDir::Horizontal => 0.0,
1337 } + bp.rot.unwrap_or(0.0);
1338 let r = if (rotation - 90.0).abs() < 1e-6 || (rotation - 270.0).abs() < 1e-6 {
1340 let c = r.center();
1341 Rect::new(
1342 c.x - r.height() / 2.0,
1343 c.y - r.width() / 2.0,
1344 c.x + r.height() / 2.0,
1345 c.y + r.width() / 2.0,
1346 )
1347 } else {
1348 r
1349 };
1350 let inner = Rect::new(
1351 r.x0 + bp.inset_l.unwrap_or(7.2),
1352 r.y0 + bp.inset_t.unwrap_or(3.6),
1353 (r.x1 - bp.inset_r.unwrap_or(7.2)).max(r.x0 + bp.inset_l.unwrap_or(7.2)),
1354 (r.y1 - bp.inset_b.unwrap_or(3.6)).max(r.y0 + bp.inset_t.unwrap_or(3.6)),
1355 );
1356 let follow = bp
1358 .warp
1359 .as_deref()
1360 .is_some_and(deckcraft_geom::warp::is_follow);
1361 let wrap = bp.wrap.unwrap_or(true)
1362 && !follow
1363 && !matches!(bp.autofit, Some(AutoFit::Shape) if shape.text_box && !bp.wrap.unwrap_or(true));
1364 let ncols = bp.columns.unwrap_or(1).clamp(1, 16) as usize;
1365 let col_gap = bp.col_spacing.unwrap_or(0.0).max(0.0);
1366 let col_w = ((inner.width() - col_gap * (ncols as f64 - 1.0)) / ncols as f64).max(1.0);
1367
1368 let mut out = TextLayout {
1369 inner,
1370 font_scale: scale,
1371 line_reduction: reduce,
1372 rotation,
1373 ..Default::default()
1374 };
1375 let mut y = 0.0;
1376 let mut numbering: Vec<u32> = vec![0; 9];
1377 let mut all_lines: Vec<(LineInfo, LinePlace, f64, Option<(String, f64, f64)>)> = vec![];
1378 let mut prev_after = 0.0;
1379 let mut shaped_paras: Vec<ParaShaped> = Vec::with_capacity(body.paragraphs.len());
1380 for (pi, para) in body.paragraphs.iter().enumerate() {
1381 let lvl = para.level.min(8) as usize;
1382 let pp = resolve::para(ctx, shape, para);
1383 let number = if matches!(pp.bullet, Some(Bullet::AutoNum { .. })) && !para.is_empty() {
1384 if let Some(n) = numbering.get_mut(lvl) {
1385 *n += 1;
1386 }
1387 for deeper in numbering.iter_mut().skip(lvl + 1) {
1388 *deeper = 0;
1389 }
1390 numbering.get(lvl).copied()
1391 } else {
1392 if !para.is_empty() {
1393 for n in numbering.iter_mut().skip(lvl) {
1394 *n = 0;
1395 }
1396 }
1397 None
1398 };
1399 shaped_paras.push(shape_para(ctx, shape, body, pi, scale, opts, number));
1400 }
1401 for (pi, p) in shaped_paras.iter().enumerate() {
1402 let props = &p.props;
1403 let margin = props.margin_left.unwrap_or(0.0).max(0.0);
1404 let indent = props.indent.unwrap_or(0.0);
1405 let mr = props.margin_right.unwrap_or(0.0).max(0.0);
1406 let bullet_w = p
1407 .bullet
1408 .as_ref()
1409 .map(|(s, st)| measure_str(&st.face, st.size, s).0)
1410 .unwrap_or(0.0);
1411 let first_x = if p.bullet.is_some() {
1412 let bx = (margin + indent).max(0.0);
1413 let stop = margin.max(-indent);
1417 if indent < 0.0 && bx + bullet_w <= stop {
1418 stop
1419 } else {
1420 bx + bullet_w + if indent >= 0.0 { 0.0 } else { 3.0 }
1421 }
1422 } else {
1423 (margin + indent).max(0.0)
1424 };
1425 let rest_x = margin;
1426 let tabs: Vec<f64> = props
1427 .tabs
1428 .as_ref()
1429 .map(|t| t.iter().map(|s| s.pos).collect())
1430 .unwrap_or_default();
1431 let first_w = (col_w - first_x - mr).max(1.0);
1432 let rest_w = (col_w - rest_x - mr).max(1.0);
1433 let raw = break_lines(
1434 p,
1435 first_w,
1436 rest_w,
1437 wrap,
1438 &tabs,
1439 props.default_tab.unwrap_or(72.0),
1440 first_x,
1441 rest_x,
1442 );
1443 let before = if pi == 0 {
1444 0.0
1445 } else {
1446 spacing_pts(props.space_before, p.lead_size, reduce)
1447 };
1448 y += before.max(0.0) + prev_after;
1449 let first_line_idx = all_lines.len();
1450 for (li, rl) in raw.iter().enumerate() {
1451 let (mut asc, mut desc, mut size) = (0.0f64, 0.0f64, 0.0f64);
1453 for c in p.cells.get(rl.start..rl.end.max(rl.start)).unwrap_or(&[]) {
1454 if let (Some(st), Some(face)) = (p.styles.get(c.style), p.faces.get(c.face)) {
1455 let sz = st.size;
1456 let (a, d) = line_metrics(face, sz);
1457 asc = asc.max(a);
1458 desc = desc.max(d);
1459 size = size.max(sz);
1460 }
1461 }
1462 if size == 0.0 {
1463 let (a, d) = line_metrics(&p.lead_face, p.lead_size);
1464 asc = a;
1465 desc = d;
1466 size = p.lead_size;
1467 }
1468 let natural = (asc + desc).max(size * 1.0);
1469 let lh = match props.line_spacing {
1470 Some(Spacing::Pct(f)) => natural * f.max(0.0) * (1.0 - reduce),
1471 Some(Spacing::Pts(v)) => v * (1.0 - reduce),
1472 None => natural * (1.0 - reduce),
1473 };
1474 let ratio = if natural > 0.0 { lh / natural } else { 1.0 };
1475 let top = y;
1476 let baseline = top + asc * ratio;
1477 y += lh;
1478 let x0 = if li == 0 { first_x } else { rest_x };
1479 let avail = (col_w - x0 - mr).max(0.0);
1480 let n = rl.end.saturating_sub(rl.start);
1481 let line_cells = p.cells.get(rl.start..rl.end).unwrap_or(&[]);
1482 let mut adv = Vec::with_capacity(n);
1484 let mut x = 0.0;
1485 for c in line_cells {
1486 let a = if c.ch == '\t' {
1487 tab_advance(x0 + x, &tabs, props.default_tab.unwrap_or(72.0))
1488 } else {
1489 c.adv
1490 };
1491 adv.push(a);
1492 x += a;
1493 }
1494 let width = x;
1495 let align = props.align.unwrap_or(Align::Left);
1496 let last_line = li + 1 == raw.len();
1497 let slack = (avail - width).max(0.0);
1498 let stretch = !last_line && wrap && slack > 0.0;
1500 let mut kashida = vec![0.0; n];
1501 let mut filled = false;
1502 if stretch && align == Align::JustLow {
1503 let ops = kashida_opportunities(p, rl.start, rl.end);
1504 if !ops.is_empty() {
1505 let each = slack / ops.len() as f64;
1506 for k in ops {
1507 if let (Some(e), Some(a)) = (kashida.get_mut(k), adv.get_mut(k)) {
1508 *e = each;
1509 *a += each;
1510 }
1511 }
1512 filled = true;
1513 }
1514 }
1515 if stretch && !filled && matches!(align, Align::Justify | Align::JustLow) {
1516 let spaces = line_cells.iter().filter(|c| c.ch == ' ').count();
1517 if spaces > 0 {
1518 for (a, c) in adv.iter_mut().zip(line_cells) {
1519 if c.ch == ' ' {
1520 *a += slack / spaces as f64;
1521 }
1522 }
1523 filled = true;
1524 }
1525 }
1526 if align == Align::Distributed && n > 1 {
1527 for a in adv.iter_mut().take(n - 1) {
1528 *a += slack / (n - 1) as f64;
1529 }
1530 filled = true;
1531 }
1532 let line_w: f64 = adv.iter().sum();
1533 let (box_l, box_r) = if p.rtl {
1536 (mr, col_w - x0)
1537 } else {
1538 (x0, col_w - mr)
1539 };
1540 let left = if filled {
1541 box_l
1542 } else {
1543 match align {
1544 Align::Left => box_l,
1545 Align::Right => box_r - line_w,
1546 Align::Center => box_l + (box_r - box_l - line_w) / 2.0,
1547 Align::Distributed if n <= 1 => box_l + slack / 2.0,
1548 Align::Justify | Align::JustLow | Align::Distributed if p.rtl => box_r - line_w,
1550 Align::Justify | Align::JustLow | Align::Distributed => box_l,
1551 }
1552 };
1553 let content_shift = if p.rtl {
1556 box_r - (left + line_w)
1557 } else {
1558 left - box_l
1559 };
1560 let chars: Vec<char> = line_cells.iter().map(|c| c.ch).collect();
1562 let raw_levels: Vec<u8> = line_cells.iter().map(|c| c.level).collect();
1563 let levels = bidi::line_levels(&chars, &raw_levels, u8::from(p.rtl));
1564 let order = bidi::visual_order(&levels);
1565 let mut edges = vec![(left, left); n];
1566 let mut vx = left;
1567 for &k in &order {
1568 let a = adv.get(k).copied().unwrap_or(0.0);
1569 if let Some(e) = edges.get_mut(k) {
1570 *e = (vx, vx + a);
1571 }
1572 vx += a;
1573 }
1574 let odd = |k: usize| levels.get(k).is_some_and(|l| l % 2 == 1);
1575 let mut caret_x: Vec<f64> = (0..n)
1576 .map(|k| {
1577 edges
1578 .get(k)
1579 .map(|e| if odd(k) { e.1 } else { e.0 })
1580 .unwrap_or(left)
1581 })
1582 .collect();
1583 caret_x.push(match n.checked_sub(1) {
1584 Some(k) => edges
1585 .get(k)
1586 .map(|e| if odd(k) { e.0 } else { e.1 })
1587 .unwrap_or(left),
1588 None if p.rtl => left + line_w,
1589 None => left,
1590 });
1591 let bullet = if li == 0 {
1592 p.bullet.as_ref().map(|(s, _)| {
1593 let end_aligned = if p.rtl {
1595 align == Align::Left
1596 } else {
1597 align == Align::Right
1598 };
1599 let shifted = align == Align::Center || end_aligned;
1600 let b = if shifted && p.cells.is_empty() {
1601 content_shift + x0 - bullet_w
1602 } else {
1603 (margin + indent).max(0.0) + if shifted { content_shift } else { 0.0 }
1604 };
1605 let bx = if p.rtl { col_w - b - bullet_w } else { b };
1607 (s.clone(), bx, baseline)
1608 })
1609 } else {
1610 None
1611 };
1612 out.content_width = out.content_width.max(width + x0 + mr);
1613 let boundary: Vec<bool> = line_cells.iter().map(|c| c.boundary).collect();
1614 all_lines.push((
1615 LineInfo {
1616 para: pi,
1617 start: rl.start,
1618 end: rl.end,
1619 top,
1620 baseline,
1621 bottom: y,
1622 caret_x,
1623 column: 0,
1624 rtl: p.rtl,
1625 levels,
1626 edges,
1627 boundary,
1628 extent: (left, left + line_w),
1629 start_offset: x0,
1630 },
1631 LinePlace { order, kashida },
1632 lh,
1633 bullet,
1634 ));
1635 }
1636 out.para_lines.push((first_line_idx, raw.len()));
1637 prev_after = spacing_pts(props.space_after, p.lead_size, reduce).max(0.0);
1638 }
1639 let total = y;
1640 out.content_height = total;
1641 let col_h = if ncols > 1 {
1643 inner.height()
1644 } else {
1645 f64::INFINITY
1646 };
1647 let mut col = 0usize;
1648 let mut col_y0 = 0.0;
1649 for (li, _, _, _) in all_lines.iter_mut() {
1650 if ncols > 1 && li.bottom - col_y0 > col_h + 0.01 && col + 1 < ncols {
1651 col += 1;
1652 col_y0 = li.top;
1653 }
1654 li.column = col;
1655 if col > 0 {
1656 let dy = col_y0;
1657 li.top -= dy;
1658 li.baseline -= dy;
1659 li.bottom -= dy;
1660 li.shift_x(col as f64 * (col_w + col_gap));
1661 }
1662 }
1663 let used_h = if ncols > 1 {
1664 all_lines.iter().map(|(l, ..)| l.bottom).fold(0.0, f64::max)
1665 } else {
1666 total
1667 };
1668 out.overflow = used_h > inner.height() + 0.5
1669 || (ncols > 1
1670 && all_lines
1671 .last()
1672 .is_some_and(|(l, ..)| l.bottom > inner.height() + 0.5));
1673 let anchor = bp.anchor.unwrap_or(Anchor::Top);
1674 let dy = match anchor {
1675 Anchor::Top | Anchor::Justified | Anchor::Distributed => 0.0,
1676 Anchor::Middle => (inner.height() - used_h) / 2.0,
1677 Anchor::Bottom => inner.height() - used_h,
1678 };
1679 let dx = if bp.anchor_ctr.unwrap_or(false) {
1681 ((inner.width() - out.content_width) / 2.0).max(0.0)
1682 } else {
1683 0.0
1684 };
1685 let ox = inner.x0 + dx;
1686 let oy = inner.y0 + dy;
1687 for (line, place, _, bullet) in all_lines.iter_mut() {
1689 line.top += oy;
1690 line.baseline += oy;
1691 line.bottom += oy;
1692 line.shift_x(ox);
1693 let Some(p) = shaped_paras.get(line.para) else {
1694 continue;
1695 };
1696 if let Some((s, bx, by)) = bullet.take()
1697 && let Some((_, st)) = p.bullet.as_ref()
1698 {
1699 let (_, g) = measure_str(&st.face, st.size, &s);
1700 out.runs.push(GlyphRun {
1701 face: st.face.clone(),
1702 size: st.size,
1703 color: st.color,
1704 glyphs: g
1705 .into_iter()
1706 .map(|(gid, x)| {
1707 (
1708 gid,
1709 ox + bx
1710 + x
1711 + if line.column > 0 {
1712 line.column as f64 * (col_w + col_gap)
1713 } else {
1714 0.0
1715 },
1716 by + oy,
1717 )
1718 })
1719 .collect(),
1720 cells: vec![],
1721 fake_bold: false,
1722 fake_italic: false,
1723 outline: None,
1724 para: line.para,
1725 chars: (0, 0),
1726 link: false,
1727 alpha: st.alpha,
1728 gradient: None,
1729 effects: None,
1730 });
1731 }
1732 let mut cur: Option<GlyphRun> = None;
1734 let mut cur_key = (usize::MAX, usize::MAX);
1735 for &k in &place.order {
1736 let ci = line.start + k;
1737 let Some(cell) = p.cells.get(ci) else {
1738 continue;
1739 };
1740 let Some(st) = p.styles.get(cell.style) else {
1741 continue;
1742 };
1743 let Some(&(cx, cx1)) = line.edges.get(k) else {
1744 continue;
1745 };
1746 let base_y = line.baseline - st.baseline * st.size;
1747 let size = if st.baseline != 0.0 {
1748 st.size * SUB_SUPER
1749 } else {
1750 st.size
1751 };
1752 let size = if st.caps == Caps::Small && cell.ch.is_lowercase() {
1753 size * 0.8
1754 } else {
1755 size
1756 };
1757 let key = (cell.style, cell.face);
1758 if key != cur_key {
1759 if let Some(r) = cur.take() {
1760 out.runs.push(r);
1761 }
1762 let Some(face) = p.faces.get(cell.face) else {
1763 continue;
1764 };
1765 cur = Some(GlyphRun {
1766 face: face.clone(),
1767 size,
1768 color: st.color,
1769 glyphs: vec![],
1770 cells: vec![],
1771 fake_bold: st.fake_bold,
1772 fake_italic: st.fake_italic,
1773 outline: st.outline,
1774 para: line.para,
1775 chars: (ci, ci + 1),
1776 link: st.link,
1777 alpha: st.alpha,
1778 gradient: st.gradient.clone(),
1779 effects: st.effects,
1780 });
1781 cur_key = key;
1782 }
1783 if let Some(r) = cur.as_mut() {
1784 r.chars = (r.chars.0.min(ci), r.chars.1.max(ci + 1));
1785 if cell.head == ci && !cell.glyphs.is_empty() {
1786 let cells_of = (k..line.end.saturating_sub(line.start))
1789 .take_while(|&j| p.cells.get(line.start + j).is_some_and(|c| c.head == ci));
1790 let (mut cl, mut kash) = (cx, 0.0);
1791 for j in cells_of {
1792 cl = cl.min(line.edges.get(j).map(|e| e.0).unwrap_or(cx));
1793 kash += place.kashida.get(j).copied().unwrap_or(0.0);
1794 }
1795 if kash > 0.0 {
1796 let face = p.faces.get(cell.face);
1797 let tg = face.map(|f| f.glyph_for('\u{0640}')).unwrap_or(0);
1798 let tw = face
1799 .map(|f| f.advance(tg) * size / f.upem.max(1.0))
1800 .unwrap_or(0.0);
1801 if tg != 0 && tw > 0.0 {
1802 let count = ((kash / tw).ceil() as usize).clamp(1, 256);
1803 for i in 0..count {
1804 r.glyphs.push((
1805 tg,
1806 cl + (i as f64 * tw).min((kash - tw).max(0.0)),
1807 base_y,
1808 ));
1809 r.cells.push((ci, cell.ch));
1810 }
1811 }
1812 }
1813 for (gid, gx, gy) in &cell.glyphs {
1814 r.glyphs.push((*gid, cl + kash + gx, base_y + gy));
1815 r.cells.push((ci, cell.ch));
1816 }
1817 }
1818 }
1819 let adv = cx1 - cx;
1820 if adv <= 0.0 {
1821 continue;
1822 }
1823 if let Some(hl) = st.highlight {
1824 out.decos.push(Deco {
1825 rect: Rect::new(cx, line.top, cx1, line.bottom),
1826 color: hl,
1827 behind: true,
1828 para: line.para,
1829 });
1830 }
1831 if !cell.ch.is_whitespace() || st.underline.is_some() {
1832 if let Some(uc) = st.underline {
1833 let t = (st.size * 0.06).max(0.5);
1834 let uy = line.baseline + st.size * 0.12;
1835 out.decos.push(Deco {
1836 rect: Rect::new(cx, uy, cx1, uy + t),
1837 color: uc,
1838 behind: false,
1839 para: line.para,
1840 });
1841 }
1842 if let Some(sk) = st.strike {
1843 let t = (st.size * 0.05).max(0.5);
1844 let sy = line.baseline - st.size * 0.3;
1845 out.decos.push(Deco {
1846 rect: Rect::new(cx, sy, cx1, sy + t),
1847 color: st.color,
1848 behind: false,
1849 para: line.para,
1850 });
1851 if sk == Strike::Double {
1852 out.decos.push(Deco {
1853 rect: Rect::new(cx, sy - t * 2.0, cx1, sy - t),
1854 color: st.color,
1855 behind: false,
1856 para: line.para,
1857 });
1858 }
1859 }
1860 }
1861 }
1862 if let Some(r) = cur.take() {
1863 out.runs.push(r);
1864 }
1865 }
1866 out.boundaries = shaped_paras
1867 .iter()
1868 .map(|p| {
1869 let mut b: Vec<bool> = p.cells.iter().map(|c| c.boundary).collect();
1870 b.push(true);
1871 if let Some(f) = b.first_mut() {
1872 *f = true;
1873 }
1874 b
1875 })
1876 .collect();
1877 out.lines = all_lines.into_iter().map(|(l, ..)| l).collect();
1878 out.decos = merge_decos(std::mem::take(&mut out.decos));
1880 out
1881}
1882
1883fn merge_decos(v: Vec<Deco>) -> Vec<Deco> {
1884 let mut out: Vec<Deco> = Vec::with_capacity(v.len());
1885 for d in v {
1886 if let Some(last) = out.last_mut()
1887 && last.color == d.color
1888 && last.behind == d.behind
1889 && (last.rect.y0 - d.rect.y0).abs() < 0.01
1890 && (last.rect.y1 - d.rect.y1).abs() < 0.01
1891 && (last.rect.x1 - d.rect.x0).abs() < 0.5
1892 {
1893 last.rect.x1 = d.rect.x1;
1894 continue;
1895 }
1896 out.push(d);
1897 }
1898 out
1899}
1900
1901struct LinePlace {
1904 order: Vec<usize>,
1905 kashida: Vec<f64>,
1906}
1907
1908fn kashida_opportunities(p: &ParaShaped, start: usize, end: usize) -> Vec<usize> {
1912 let mut out: Vec<usize> = Vec::new();
1913 let mut word_has = false;
1914 for i in start..end {
1915 let Some(c) = p.cells.get(i) else { continue };
1916 if c.ch.is_whitespace() {
1917 word_has = false;
1918 continue;
1919 }
1920 if !c.boundary || c.level % 2 == 0 || !joins_following(c.ch) {
1921 continue;
1922 }
1923 let Some(nx) = (i + 1..end).find(|&j| p.cells.get(j).is_some_and(|d| d.boundary)) else {
1925 continue;
1926 };
1927 let Some(d) = p.cells.get(nx) else { continue };
1928 let same_cluster = d.head == c.head || d.head != nx;
1929 let tatweel = p
1930 .faces
1931 .get(c.face)
1932 .is_some_and(|f| f.glyph_for('\u{0640}') != 0);
1933 if joins_preceding(d.ch) && d.face == c.face && !same_cluster && tatweel {
1934 if word_has {
1936 out.pop();
1937 }
1938 out.push(nx - 1 - start);
1939 word_has = true;
1940 }
1941 }
1942 out
1943}
1944
1945#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1949pub enum Affinity {
1950 Upstream,
1951 #[default]
1952 Downstream,
1953}
1954
1955#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1957pub enum MoveMode {
1958 #[default]
1960 Logical,
1961 Visual,
1963}
1964
1965impl LineInfo {
1966 fn len(&self) -> usize {
1967 self.end.saturating_sub(self.start)
1968 }
1969 fn odd(&self, k: usize) -> bool {
1970 self.levels.get(k).is_some_and(|l| l % 2 == 1)
1971 }
1972 fn leading(&self, k: usize) -> Option<f64> {
1974 self.edges
1975 .get(k)
1976 .map(|e| if self.odd(k) { e.1 } else { e.0 })
1977 }
1978 fn trailing(&self, k: usize) -> Option<f64> {
1980 self.edges
1981 .get(k)
1982 .map(|e| if self.odd(k) { e.0 } else { e.1 })
1983 }
1984 fn shift_x(&mut self, dx: f64) {
1985 for x in &mut self.caret_x {
1986 *x += dx;
1987 }
1988 for e in &mut self.edges {
1989 e.0 += dx;
1990 e.1 += dx;
1991 }
1992 self.extent.0 += dx;
1993 self.extent.1 += dx;
1994 }
1995 fn graphemes(&self) -> Vec<(usize, usize)> {
1997 let n = self.len();
1998 let mut out = Vec::new();
1999 let mut k = 0;
2000 while k < n {
2001 let mut e = k + 1;
2002 while e < n && !self.boundary.get(e).copied().unwrap_or(true) {
2003 e += 1;
2004 }
2005 out.push((k, e));
2006 k = e;
2007 }
2008 out
2009 }
2010 fn grapheme_box(&self, from: usize, to: usize) -> Option<(f64, f64)> {
2012 let es = self.edges.get(from..to)?;
2013 let l = es.iter().map(|e| e.0).fold(f64::INFINITY, f64::min);
2014 let r = es.iter().map(|e| e.1).fold(f64::NEG_INFINITY, f64::max);
2015 (l <= r).then_some((l, r))
2016 }
2017 pub fn spans(&self, from: usize, to: usize) -> Vec<(f64, f64)> {
2020 let a = from.max(self.start) - self.start;
2021 let b = to.min(self.end).saturating_sub(self.start);
2022 merge_spans(self.edges.get(a..b.max(a)).unwrap_or(&[]).to_vec())
2023 }
2024 pub fn span(&self, from: usize, to: usize) -> Option<(f64, f64)> {
2026 let s = self.spans(from, to);
2027 Some((s.first()?.0, s.last()?.1))
2028 }
2029 fn visual_stops(&self) -> Vec<(f64, usize, Affinity)> {
2031 let mut gs: Vec<(f64, f64, usize, usize, bool)> = self
2032 .graphemes()
2033 .into_iter()
2034 .filter_map(|(a, b)| {
2035 self.grapheme_box(a, b)
2036 .map(|(l, r)| (l, r, a, b, self.odd(a)))
2037 })
2038 .collect();
2039 gs.sort_by(|x, y| x.0.total_cmp(&y.0));
2040 let mut out: Vec<(f64, usize, Affinity)> = Vec::with_capacity(gs.len() * 2 + 1);
2041 let mut push = |s: (f64, usize, Affinity)| {
2042 if !out
2043 .last()
2044 .is_some_and(|l: &(f64, usize, Affinity)| (l.0 - s.0).abs() < 0.01 && l.1 == s.1)
2045 {
2046 out.push(s);
2047 }
2048 };
2049 for (l, r, a, b, rtl) in gs {
2050 let (a, b) = (self.start + a, self.start + b);
2051 if rtl {
2052 push((l, b, Affinity::Upstream));
2053 push((r, a, Affinity::Downstream));
2054 } else {
2055 push((l, a, Affinity::Downstream));
2056 push((r, b, Affinity::Upstream));
2057 }
2058 }
2059 if out.is_empty() {
2060 out.push((
2061 self.caret_x.first().copied().unwrap_or(0.0),
2062 self.start,
2063 Affinity::Downstream,
2064 ));
2065 }
2066 out
2067 }
2068}
2069
2070fn merge_spans(mut v: Vec<(f64, f64)>) -> Vec<(f64, f64)> {
2072 v.retain(|e| e.1 > e.0);
2073 v.sort_by(|x, y| x.0.total_cmp(&y.0));
2074 let mut out: Vec<(f64, f64)> = Vec::with_capacity(v.len());
2075 for e in v {
2076 match out.last_mut() {
2077 Some(last) if e.0 <= last.1 + 0.01 => last.1 = last.1.max(e.1),
2078 _ => out.push(e),
2079 }
2080 }
2081 out
2082}
2083
2084#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
2086pub struct Pos {
2087 pub para: usize,
2088 pub ch: usize,
2089}
2090
2091impl TextLayout {
2092 pub fn line_of(&self, pos: Pos) -> Option<usize> {
2094 self.line_of_affine(pos, Affinity::Downstream)
2095 }
2096 pub fn line_of_affine(&self, pos: Pos, affinity: Affinity) -> Option<usize> {
2099 let mut found = None;
2100 for (i, l) in self.lines.iter().enumerate() {
2101 if l.para == pos.para && pos.ch >= l.start && pos.ch <= l.end {
2102 if affinity == Affinity::Upstream && pos.ch == l.end && pos.ch > l.start {
2103 return Some(i);
2104 }
2105 found = Some(i);
2106 if pos.ch < l.end {
2107 break;
2108 }
2109 }
2110 }
2111 found
2112 .or_else(|| {
2113 self.lines
2114 .iter()
2115 .rposition(|l| l.para == pos.para && l.start <= pos.ch)
2116 })
2117 .or_else(|| self.lines.iter().rposition(|l| l.para == pos.para))
2118 }
2119 pub fn caret(&self, pos: Pos) -> Option<(f64, f64, f64)> {
2121 self.caret_at(pos, Affinity::Downstream)
2122 }
2123 pub fn caret_at(&self, pos: Pos, affinity: Affinity) -> Option<(f64, f64, f64)> {
2126 let li = self.line_of_affine(pos, affinity)?;
2127 let l = self.lines.get(li)?;
2128 let k = pos.ch.clamp(l.start, l.end) - l.start;
2129 let x = match affinity {
2130 Affinity::Downstream if k < l.len() => l.leading(k),
2131 Affinity::Upstream if k > 0 => l.trailing(k - 1),
2132 _ => None,
2133 };
2134 let x = x
2135 .or_else(|| l.caret_x.get(k).copied())
2136 .or(l.caret_x.last().copied())?;
2137 Some((x, l.top, l.bottom))
2138 }
2139 pub fn hit(&self, p: Point) -> Pos {
2141 self.hit_affinity(p).0
2142 }
2143 pub fn hit_affinity(&self, p: Point) -> (Pos, Affinity) {
2146 let mut best: Option<&LineInfo> = None;
2147 for l in &self.lines {
2148 if p.y >= l.top && p.y < l.bottom {
2149 if best.is_none()
2151 || p.x
2152 >= l.extent
2153 .0
2154 .min(l.caret_x.first().copied().unwrap_or(l.extent.0))
2155 - 4.0
2156 {
2157 best = Some(l);
2158 }
2159 }
2160 }
2161 let l = match best {
2162 Some(l) => l,
2163 None => {
2164 let first = self.lines.first();
2165 let last = self.lines.last();
2166 match (first, last) {
2167 (Some(f), _) if p.y < f.top => f,
2168 (_, Some(l)) => l,
2169 _ => return (Pos::default(), Affinity::Downstream),
2170 }
2171 }
2172 };
2173 let mut gs: Vec<(f64, f64, usize, usize, bool)> = l
2174 .graphemes()
2175 .into_iter()
2176 .filter_map(|(a, b)| {
2177 l.grapheme_box(a, b)
2178 .map(|(x0, x1)| (x0, x1, a, b, l.odd(a)))
2179 })
2180 .collect();
2181 if gs.is_empty() {
2182 return (
2183 Pos {
2184 para: l.para,
2185 ch: l.start,
2186 },
2187 Affinity::Downstream,
2188 );
2189 }
2190 gs.sort_by(|x, y| x.0.total_cmp(&y.0));
2191 let hit = gs
2192 .iter()
2193 .find(|g| p.x >= g.0 && p.x <= g.1 && g.1 > g.0)
2194 .copied();
2195 let (g, left_half) = match hit {
2196 Some(g) => (g, p.x < (g.0 + g.1) / 2.0),
2197 None => match (gs.first().copied(), gs.last().copied()) {
2198 (Some(f), _) if p.x < f.0 => (f, true),
2199 (_, Some(z)) if p.x > z.1 => (z, false),
2200 _ => match gs
2202 .iter()
2203 .min_by(|a, b| {
2204 ((a.0 + a.1) / 2.0 - p.x)
2205 .abs()
2206 .total_cmp(&((b.0 + b.1) / 2.0 - p.x).abs())
2207 })
2208 .copied()
2209 {
2210 Some(g) => (g, p.x < (g.0 + g.1) / 2.0),
2211 None => {
2212 return (
2213 Pos {
2214 para: l.para,
2215 ch: l.start,
2216 },
2217 Affinity::Downstream,
2218 );
2219 }
2220 },
2221 },
2222 };
2223 let (_, _, a, b, rtl) = g;
2224 let (before, after) = (
2225 Pos {
2226 para: l.para,
2227 ch: l.start + a,
2228 },
2229 Pos {
2230 para: l.para,
2231 ch: l.start + b,
2232 },
2233 );
2234 if left_half != rtl {
2236 (before, Affinity::Downstream)
2237 } else {
2238 (after, Affinity::Upstream)
2239 }
2240 }
2241 pub fn move_caret(
2245 &self,
2246 pos: Pos,
2247 affinity: Affinity,
2248 forward: bool,
2249 mode: MoveMode,
2250 ) -> (Pos, Affinity) {
2251 if mode == MoveMode::Visual
2252 && let Some(li) = self.line_of_affine(pos, affinity)
2253 && let Some(l) = self.lines.get(li)
2254 {
2255 let stops = l.visual_stops();
2256 let cur_x = self.caret_at(pos, affinity).map(|c| c.0).unwrap_or(0.0);
2257 let idx = stops
2258 .iter()
2259 .position(|s| s.1 == pos.ch && s.2 == affinity)
2260 .or_else(|| stops.iter().position(|s| s.1 == pos.ch))
2261 .or_else(|| {
2262 stops
2263 .iter()
2264 .enumerate()
2265 .min_by(|a, b| (a.1.0 - cur_x).abs().total_cmp(&(b.1.0 - cur_x).abs()))
2266 .map(|(i, _)| i)
2267 });
2268 if let Some(idx) = idx {
2269 let mut j = idx;
2270 loop {
2271 let next = if forward {
2272 j.checked_add(1)
2273 } else {
2274 j.checked_sub(1)
2275 };
2276 let Some(nj) = next else { break };
2277 let Some(s) = stops.get(nj) else { break };
2278 if s.1 != pos.ch && (s.0 - cur_x).abs() > 0.01 {
2280 return (
2281 Pos {
2282 para: l.para,
2283 ch: s.1,
2284 },
2285 s.2,
2286 );
2287 }
2288 j = nj;
2289 }
2290 }
2291 let logical_forward = forward != l.rtl;
2293 let target = if logical_forward {
2294 li.checked_add(1)
2295 } else {
2296 li.checked_sub(1)
2297 };
2298 return match target.and_then(|t| self.lines.get(t)) {
2299 Some(t) if logical_forward => (
2300 Pos {
2301 para: t.para,
2302 ch: t.start,
2303 },
2304 Affinity::Downstream,
2305 ),
2306 Some(t) => (
2307 Pos {
2308 para: t.para,
2309 ch: t.end,
2310 },
2311 if t.para == l.para {
2312 Affinity::Upstream
2313 } else {
2314 Affinity::Downstream
2315 },
2316 ),
2317 None => (pos, affinity),
2318 };
2319 }
2320 let Some(b) = self.boundaries.get(pos.para) else {
2321 return (pos, affinity);
2322 };
2323 let len = b.len().saturating_sub(1);
2324 let ch = pos.ch.min(len);
2325 let next = if forward {
2326 (ch + 1..=len).find(|&k| b.get(k).copied().unwrap_or(true))
2327 } else {
2328 (0..ch).rev().find(|&k| b.get(k).copied().unwrap_or(true))
2329 };
2330 match next {
2331 Some(k) => (
2332 Pos {
2333 para: pos.para,
2334 ch: k,
2335 },
2336 Affinity::Downstream,
2337 ),
2338 None if forward && pos.para + 1 < self.boundaries.len() => (
2339 Pos {
2340 para: pos.para + 1,
2341 ch: 0,
2342 },
2343 Affinity::Downstream,
2344 ),
2345 None if !forward && pos.para > 0 => {
2346 let prev = pos.para - 1;
2347 (
2348 Pos {
2349 para: prev,
2350 ch: self
2351 .boundaries
2352 .get(prev)
2353 .map(|b| b.len().saturating_sub(1))
2354 .unwrap_or(0),
2355 },
2356 Affinity::Downstream,
2357 )
2358 }
2359 None => (Pos { para: pos.para, ch }, Affinity::Downstream),
2360 }
2361 }
2362 pub fn selection_rects(&self, a: Pos, b: Pos) -> Vec<Rect> {
2365 let (s, e) = if a <= b { (a, b) } else { (b, a) };
2366 let mut out = vec![];
2367 for l in &self.lines {
2368 let lp = Pos {
2369 para: l.para,
2370 ch: l.start,
2371 };
2372 let le = Pos {
2373 para: l.para,
2374 ch: l.end,
2375 };
2376 if le < s || lp > e {
2377 continue;
2378 }
2379 let from = if s > lp { s.ch } else { l.start };
2380 let to = if e < le { e.ch } else { l.end };
2381 let mut spans = l.spans(from, to);
2382 if e.para > l.para && to == l.end {
2385 let end_x = l.caret_x.last().copied().unwrap_or(l.extent.1);
2386 spans.push(if l.rtl {
2387 (end_x - 6.0, end_x)
2388 } else {
2389 (end_x, end_x + 6.0)
2390 });
2391 spans = merge_spans(spans);
2392 }
2393 for (x0, x1) in spans {
2394 if x1 > x0 {
2395 out.push(Rect::new(x0, l.top, x1, l.bottom));
2396 }
2397 }
2398 }
2399 out
2400 }
2401 pub fn composition_underlines(&self, a: Pos, b: Pos) -> Vec<(f64, f64, f64)> {
2405 let (s, e) = if a <= b { (a, b) } else { (b, a) };
2406 let mut out = vec![];
2407 if s == e {
2408 return out;
2409 }
2410 for l in &self.lines {
2411 let lp = Pos {
2412 para: l.para,
2413 ch: l.start,
2414 };
2415 let le = Pos {
2416 para: l.para,
2417 ch: l.end,
2418 };
2419 if le <= s || lp >= e {
2420 continue;
2421 }
2422 let from = if s > lp { s.ch } else { l.start };
2423 let to = if e < le { e.ch } else { l.end };
2424 let y = (l.baseline + (l.bottom - l.baseline) * 0.35).min(l.bottom);
2426 for (x0, x1) in l.spans(from, to) {
2427 if x1 > x0 {
2428 out.push((x0, x1, y));
2429 }
2430 }
2431 }
2432 out
2433 }
2434 pub fn vertical(&self, pos: Pos, down: bool, x: f64) -> Pos {
2436 self.vertical_affine(pos, Affinity::Downstream, down, x).0
2437 }
2438 pub fn line_index(&self, pos: Pos, affinity: Affinity) -> Option<usize> {
2441 if affinity == Affinity::Upstream && pos.ch > 0 {
2442 for (i, w) in self.lines.windows(2).enumerate() {
2443 if let [a, b] = w
2444 && b.para == pos.para
2445 && a.para == pos.para
2446 && b.start == pos.ch
2447 {
2448 return Some(i);
2449 }
2450 }
2451 }
2452 self.line_of_affine(pos, affinity)
2453 }
2454 pub fn vertical_affine(
2456 &self,
2457 pos: Pos,
2458 affinity: Affinity,
2459 down: bool,
2460 x: f64,
2461 ) -> (Pos, Affinity) {
2462 let Some(li) = self.line_index(pos, affinity) else {
2463 return (pos, affinity);
2464 };
2465 let target = if down {
2466 li.checked_add(1)
2467 } else {
2468 li.checked_sub(1)
2469 };
2470 let Some(l) = target.and_then(|t| self.lines.get(t)) else {
2471 return (pos, affinity);
2472 };
2473 let p = Point::new(x, (l.top + l.bottom) / 2.0);
2474 let mut h = self.hit(p);
2475 if h.para != l.para {
2476 h.para = l.para;
2477 }
2478 h.ch = h.ch.clamp(l.start, l.end);
2479 (h, Affinity::Downstream)
2480 }
2481 pub fn line_bounds(&self, pos: Pos) -> (Pos, Pos) {
2483 self.line_bounds_affine(pos, Affinity::Downstream)
2484 }
2485 pub fn line_bounds_affine(&self, pos: Pos, affinity: Affinity) -> (Pos, Pos) {
2487 match self
2488 .line_index(pos, affinity)
2489 .and_then(|i| self.lines.get(i))
2490 {
2491 Some(l) => (
2492 Pos {
2493 para: l.para,
2494 ch: l.start,
2495 },
2496 Pos {
2497 para: l.para,
2498 ch: l.end,
2499 },
2500 ),
2501 None => (pos, pos),
2502 }
2503 }
2504 pub fn line_end_caret(&self, pos: Pos, affinity: Affinity) -> (Pos, Affinity) {
2508 let Some(li) = self.line_index(pos, affinity) else {
2509 return (pos, Affinity::Downstream);
2510 };
2511 let Some(l) = self.lines.get(li) else {
2512 return (pos, Affinity::Downstream);
2513 };
2514 match self.lines.get(li + 1) {
2515 Some(n) if n.para == l.para && n.start > l.start => (
2516 Pos {
2517 para: l.para,
2518 ch: n.start,
2519 },
2520 Affinity::Upstream,
2521 ),
2522 _ => (
2523 Pos {
2524 para: l.para,
2525 ch: l.end,
2526 },
2527 Affinity::Downstream,
2528 ),
2529 }
2530 }
2531 pub fn caret_drawn(&self, pos: Pos, affinity: Affinity) -> Option<(f64, f64, f64)> {
2534 let li = self.line_index(pos, affinity)?;
2535 let l = self.lines.get(li)?;
2536 if pos.ch > l.end {
2537 let x = l.caret_x.last().copied()?;
2538 return Some((x, l.top, l.bottom));
2539 }
2540 self.caret_at(pos, affinity)
2541 }
2542 pub fn bounds(&self) -> Rect {
2544 let mut r: Option<Rect> = None;
2545 for l in &self.lines {
2546 let lr = Rect::new(l.extent.0, l.top, l.extent.1.max(l.extent.0), l.bottom);
2547 r = Some(r.map(|a| a.union(lr)).unwrap_or(lr));
2548 }
2549 r.unwrap_or(self.inner)
2550 }
2551}
2552
2553pub fn fit_height(
2555 ctx: &Ctx,
2556 shape: &Shape,
2557 body: &TextBody,
2558 rect: Rect,
2559 fields: &dyn Fields,
2560) -> f64 {
2561 let bp = resolve::body(ctx, shape);
2562 let l = layout(
2563 ctx,
2564 shape,
2565 body,
2566 &Opts {
2567 rect,
2568 fields,
2569 prompt_color: None,
2570 no_shrink: true,
2571 },
2572 );
2573 l.content_height + bp.inset_t.unwrap_or(3.6) + bp.inset_b.unwrap_or(3.6)
2574}
2575
2576pub fn fit_width(
2578 ctx: &Ctx,
2579 shape: &Shape,
2580 body: &TextBody,
2581 rect: Rect,
2582 fields: &dyn Fields,
2583) -> f64 {
2584 let bp = resolve::body(ctx, shape);
2585 let wide = Rect::new(rect.x0, rect.y0, rect.x0 + 100_000.0, rect.y1);
2586 let mut s2 = shape.clone();
2587 if let Some(t) = s2.text.as_mut() {
2588 t.body.wrap = Some(false);
2589 }
2590 let l = layout(
2591 ctx,
2592 &s2,
2593 body,
2594 &Opts {
2595 rect: wide,
2596 fields,
2597 prompt_color: None,
2598 no_shrink: true,
2599 },
2600 );
2601 let w = l
2602 .lines
2603 .iter()
2604 .map(|li| li.start_offset + (li.extent.1 - li.extent.0))
2605 .fold(0.0, f64::max);
2606 w + bp.inset_l.unwrap_or(7.2) + bp.inset_r.unwrap_or(7.2) + 1.0
2607}
2608
2609#[cfg(test)]
2610mod tests;