1use crate::TextFontService;
46use crate::font::resolve::resolve_font;
47use crate::layout::block::BlockLayoutParams;
48use crate::layout::flow::{FlowItem, FlowLayout};
49use crate::layout::frame::FrameLayoutParams;
50use crate::layout::inline_markup::{InlineAttrs, InlineMarkup};
51use crate::layout::paragraph::{Alignment, break_into_lines};
52use crate::layout::table::TableLayoutParams;
53use crate::shaping::run::{ShapedGlyph, ShapedRun};
54use crate::shaping::shaper::{bidi_runs, font_metrics_px, shape_text, shape_text_with_fallback};
55use crate::types::{
56 BlockVisualInfo, CharacterGeometry, CursorDisplay, DecorationKind, DecorationRect, GlyphQuad,
57 HitTestResult, LaidOutSpan, LaidOutSpanKind, ParagraphResult, RenderFrame, SingleLineResult,
58 TextFormat,
59};
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum RelayoutError {
73 NoLayout,
78 ScaleDirty,
86}
87
88impl std::fmt::Display for RelayoutError {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 RelayoutError::NoLayout => {
92 f.write_str("relayout_block called before any layout_* method")
93 }
94 RelayoutError::ScaleDirty => f.write_str(
95 "relayout_block called after a scale-factor change without a fresh layout_*",
96 ),
97 }
98 }
99}
100
101impl std::error::Error for RelayoutError {}
102
103#[derive(Debug, Clone, Copy, Default)]
108pub enum ContentWidthMode {
109 #[default]
113 Auto,
114 Fixed(f32),
118}
119
120pub struct DocumentFlow {
127 flow_layout: FlowLayout,
128 render_frame: RenderFrame,
129 scroll_offset: f32,
130 rendered_scroll_offset: f32,
131 viewport_width: f32,
132 viewport_height: f32,
133 content_width_mode: ContentWidthMode,
134 selection_color: [f32; 4],
135 cursor_color: [f32; 4],
136 text_color: [f32; 4],
137 cursors: Vec<CursorDisplay>,
138 zoom: f32,
139 rendered_zoom: f32,
140 layout_scale_generation: u64,
146 has_layout: bool,
148}
149
150impl DocumentFlow {
151 pub fn new() -> Self {
157 Self {
158 flow_layout: FlowLayout::new(),
159 render_frame: RenderFrame::new(),
160 scroll_offset: 0.0,
161 rendered_scroll_offset: f32::NAN,
162 viewport_width: 0.0,
163 viewport_height: 0.0,
164 content_width_mode: ContentWidthMode::Auto,
165 selection_color: [0.26, 0.52, 0.96, 0.3],
166 cursor_color: [0.0, 0.0, 0.0, 1.0],
167 text_color: [0.0, 0.0, 0.0, 1.0],
168 cursors: Vec::new(),
169 zoom: 1.0,
170 rendered_zoom: f32::NAN,
171 layout_scale_generation: 0,
172 has_layout: false,
173 }
174 }
175
176 pub fn set_viewport(&mut self, width: f32, height: f32) {
192 self.viewport_width = width;
193 self.viewport_height = height;
194 self.flow_layout.viewport_width = width;
195 self.flow_layout.viewport_height = height;
196 }
197
198 pub fn viewport_width(&self) -> f32 {
200 self.viewport_width
201 }
202
203 pub fn viewport_height(&self) -> f32 {
205 self.viewport_height
206 }
207
208 pub fn set_content_width(&mut self, width: f32) {
214 self.content_width_mode = ContentWidthMode::Fixed(width);
215 }
216
217 pub fn set_content_width_auto(&mut self) {
222 self.content_width_mode = ContentWidthMode::Auto;
223 }
224
225 pub fn layout_width(&self) -> f32 {
233 match self.content_width_mode {
234 ContentWidthMode::Auto => self.viewport_width / self.zoom,
235 ContentWidthMode::Fixed(w) => w,
236 }
237 }
238
239 pub fn content_width_mode(&self) -> ContentWidthMode {
241 self.content_width_mode
242 }
243
244 pub fn set_scroll_offset(&mut self, offset: f32) {
248 self.scroll_offset = offset;
249 }
250
251 pub fn scroll_offset(&self) -> f32 {
253 self.scroll_offset
254 }
255
256 pub fn content_height(&self) -> f32 {
258 self.flow_layout.content_height
259 }
260
261 pub fn max_content_width(&self) -> f32 {
265 self.flow_layout.cached_max_content_width
266 }
267
268 pub fn set_zoom(&mut self, zoom: f32) {
282 self.zoom = zoom.clamp(0.1, 10.0);
283 }
284
285 pub fn zoom(&self) -> f32 {
287 self.zoom
288 }
289
290 pub fn has_layout(&self) -> bool {
297 self.has_layout
298 }
299
300 pub fn layout_dirty_for_scale(&self, service: &TextFontService) -> bool {
310 self.has_layout && self.layout_scale_generation != service.scale_generation()
311 }
312
313 #[cfg(feature = "text-document")]
322 pub fn layout_full(&mut self, service: &TextFontService, flow: &text_document::FlowSnapshot) {
323 use crate::bridge::convert_flow;
324
325 let converted = convert_flow(flow);
326
327 let mut all_items: Vec<(usize, FlowItemKind)> = Vec::new();
329 for (idx, params) in converted.blocks {
330 all_items.push((idx, FlowItemKind::Block(params)));
331 }
332 for (idx, params) in converted.tables {
333 all_items.push((idx, FlowItemKind::Table(params)));
334 }
335 for (idx, params) in converted.frames {
336 all_items.push((idx, FlowItemKind::Frame(params)));
337 }
338 all_items.sort_by_key(|(idx, _)| *idx);
339
340 let lw = self.layout_width();
341 self.flow_layout.clear();
342 self.flow_layout.viewport_width = self.viewport_width;
343 self.flow_layout.viewport_height = self.viewport_height;
344 self.flow_layout.scale_factor = service.scale_factor;
345
346 for (_idx, kind) in all_items {
347 match kind {
348 FlowItemKind::Block(params) => {
349 self.flow_layout
350 .add_block(&service.font_registry, ¶ms, lw);
351 }
352 FlowItemKind::Table(params) => {
353 self.flow_layout
354 .add_table(&service.font_registry, ¶ms, lw);
355 }
356 FlowItemKind::Frame(params) => {
357 self.flow_layout
358 .add_frame(&service.font_registry, ¶ms, lw);
359 }
360 }
361 }
362
363 self.note_layout_done(service);
364 }
365
366 pub fn layout_blocks(
372 &mut self,
373 service: &TextFontService,
374 block_params: Vec<BlockLayoutParams>,
375 ) {
376 self.flow_layout.scale_factor = service.scale_factor;
377 self.flow_layout
378 .layout_blocks(&service.font_registry, block_params, self.layout_width());
379 self.note_layout_done(service);
380 }
381
382 pub fn add_frame(&mut self, service: &TextFontService, params: &FrameLayoutParams) {
385 self.flow_layout.scale_factor = service.scale_factor;
386 self.flow_layout
387 .add_frame(&service.font_registry, params, self.layout_width());
388 self.note_layout_done(service);
389 }
390
391 pub fn add_table(&mut self, service: &TextFontService, params: &TableLayoutParams) {
393 self.flow_layout.scale_factor = service.scale_factor;
394 self.flow_layout
395 .add_table(&service.font_registry, params, self.layout_width());
396 self.note_layout_done(service);
397 }
398
399 pub fn relayout_block(
430 &mut self,
431 service: &TextFontService,
432 params: &BlockLayoutParams,
433 ) -> Result<(), RelayoutError> {
434 if !self.has_layout {
435 return Err(RelayoutError::NoLayout);
436 }
437 if self.layout_scale_generation != service.scale_generation() {
438 return Err(RelayoutError::ScaleDirty);
439 }
440 self.flow_layout.scale_factor = service.scale_factor;
441 self.flow_layout
442 .relayout_block(&service.font_registry, params, self.layout_width());
443 self.note_layout_done(service);
444 Ok(())
445 }
446
447 fn note_layout_done(&mut self, service: &TextFontService) {
448 self.has_layout = true;
449 self.layout_scale_generation = service.scale_generation();
450 }
451
452 pub fn render(&mut self, service: &mut TextFontService) -> &RenderFrame {
464 let effective_vw = self.viewport_width / self.zoom;
465 let effective_vh = self.viewport_height / self.zoom;
466 crate::render::frame::build_render_frame(
467 &self.flow_layout,
468 &service.font_registry,
469 &mut service.atlas,
470 &mut service.glyph_cache,
471 &mut service.scale_context,
472 self.scroll_offset,
473 effective_vw,
474 effective_vh,
475 &self.cursors,
476 self.cursor_color,
477 self.selection_color,
478 self.text_color,
479 &mut self.render_frame,
480 );
481 self.rendered_scroll_offset = self.scroll_offset;
482 self.rendered_zoom = self.zoom;
483 apply_zoom(&mut self.render_frame, self.zoom);
484 &self.render_frame
485 }
486
487 pub fn render_block_only(
500 &mut self,
501 service: &mut TextFontService,
502 block_id: usize,
503 ) -> &RenderFrame {
504 if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
505 || (self.zoom - self.rendered_zoom).abs() > 0.001
506 {
507 return self.render(service);
508 }
509
510 if !self.flow_layout.blocks.contains_key(&block_id) {
511 let in_table = self.flow_layout.tables.values().any(|table| {
512 table
513 .cell_layouts
514 .iter()
515 .any(|c| c.blocks.iter().any(|b| b.block_id == block_id))
516 });
517 if in_table {
518 return self.render(service);
519 }
520 let in_frame = self
521 .flow_layout
522 .frames
523 .values()
524 .any(|frame| crate::layout::flow::frame_contains_block(frame, block_id));
525 if in_frame {
526 return self.render(service);
527 }
528 }
529
530 if let Some(block) = self.flow_layout.blocks.get(&block_id) {
531 let old_height = self
532 .render_frame
533 .block_heights
534 .get(&block_id)
535 .copied()
536 .unwrap_or(block.height);
537 if (block.height - old_height).abs() > 0.001 {
538 return self.render(service);
539 }
540 }
541
542 let effective_vw = self.viewport_width / self.zoom;
543 let effective_vh = self.viewport_height / self.zoom;
544 let scale_factor = service.scale_factor;
545 let mut new_glyphs = Vec::new();
546 let mut new_images = Vec::new();
547 if let Some(block) = self.flow_layout.blocks.get(&block_id) {
548 let mut tmp = RenderFrame::new();
549 crate::render::frame::render_block_at_offset(
550 block,
551 0.0,
552 0.0,
553 &service.font_registry,
554 &mut service.atlas,
555 &mut service.glyph_cache,
556 &mut service.scale_context,
557 self.scroll_offset,
558 effective_vh,
559 self.text_color,
560 scale_factor,
561 &mut tmp,
562 );
563 new_glyphs = tmp.glyphs;
564 new_images = tmp.images;
565 }
566
567 let new_decos = if let Some(block) = self.flow_layout.blocks.get(&block_id) {
568 crate::render::decoration::generate_block_decorations(
569 block,
570 &service.font_registry,
571 self.scroll_offset,
572 effective_vh,
573 0.0,
574 0.0,
575 effective_vw,
576 self.text_color,
577 scale_factor,
578 )
579 } else {
580 Vec::new()
581 };
582
583 if let Some(entry) = self
584 .render_frame
585 .block_glyphs
586 .iter_mut()
587 .find(|(id, _)| *id == block_id)
588 {
589 entry.1 = new_glyphs;
590 }
591 if let Some(entry) = self
592 .render_frame
593 .block_images
594 .iter_mut()
595 .find(|(id, _)| *id == block_id)
596 {
597 entry.1 = new_images;
598 }
599 if let Some(entry) = self
600 .render_frame
601 .block_decorations
602 .iter_mut()
603 .find(|(id, _)| *id == block_id)
604 {
605 entry.1 = new_decos;
606 }
607
608 self.rebuild_flat_frame(service);
609 apply_zoom(&mut self.render_frame, self.zoom);
610 &self.render_frame
611 }
612
613 pub fn render_cursor_only(&mut self, service: &mut TextFontService) -> &RenderFrame {
621 if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
622 || (self.zoom - self.rendered_zoom).abs() > 0.001
623 {
624 return self.render(service);
625 }
626
627 self.render_frame.decorations.retain(|d| {
628 !matches!(
629 d.kind,
630 DecorationKind::Cursor | DecorationKind::Selection | DecorationKind::CellSelection
631 )
632 });
633
634 let effective_vw = self.viewport_width / self.zoom;
635 let effective_vh = self.viewport_height / self.zoom;
636 let mut cursor_decos = crate::render::cursor::generate_cursor_decorations(
637 &self.flow_layout,
638 &self.cursors,
639 self.scroll_offset,
640 self.cursor_color,
641 self.selection_color,
642 effective_vw,
643 effective_vh,
644 );
645 apply_zoom_decorations(&mut cursor_decos, self.zoom);
646 self.render_frame.decorations.extend(cursor_decos);
647
648 &self.render_frame
649 }
650
651 fn rebuild_flat_frame(&mut self, service: &mut TextFontService) {
652 self.render_frame.glyphs.clear();
653 self.render_frame.images.clear();
654 self.render_frame.decorations.clear();
655 for (_, glyphs) in &self.render_frame.block_glyphs {
656 self.render_frame.glyphs.extend_from_slice(glyphs);
657 }
658 for (_, images) in &self.render_frame.block_images {
659 self.render_frame.images.extend_from_slice(images);
660 }
661 for (_, decos) in &self.render_frame.block_decorations {
662 self.render_frame.decorations.extend_from_slice(decos);
663 }
664
665 for item in &self.flow_layout.flow_order {
666 match item {
667 FlowItem::Table { table_id, .. } => {
668 if let Some(table) = self.flow_layout.tables.get(table_id) {
669 let decos = crate::layout::table::generate_table_decorations(
670 table,
671 self.scroll_offset,
672 );
673 self.render_frame.decorations.extend(decos);
674 }
675 }
676 FlowItem::Frame { frame_id, .. } => {
677 if let Some(frame) = self.flow_layout.frames.get(frame_id) {
678 crate::render::frame::append_frame_border_decorations(
679 frame,
680 self.scroll_offset,
681 &mut self.render_frame.decorations,
682 );
683 }
684 }
685 FlowItem::Block { .. } => {}
686 }
687 }
688
689 let effective_vw = self.viewport_width / self.zoom;
690 let effective_vh = self.viewport_height / self.zoom;
691 let cursor_decos = crate::render::cursor::generate_cursor_decorations(
692 &self.flow_layout,
693 &self.cursors,
694 self.scroll_offset,
695 self.cursor_color,
696 self.selection_color,
697 effective_vw,
698 effective_vh,
699 );
700 self.render_frame.decorations.extend(cursor_decos);
701
702 self.render_frame.atlas_dirty = service.atlas.dirty;
703 self.render_frame.atlas_width = service.atlas.width;
704 self.render_frame.atlas_height = service.atlas.height;
705 if service.atlas.dirty {
706 let pixels = &service.atlas.pixels;
707 let needed = (service.atlas.width * service.atlas.height * 4) as usize;
708 self.render_frame.atlas_pixels.resize(needed, 0);
709 let copy_len = needed.min(pixels.len());
710 self.render_frame.atlas_pixels[..copy_len].copy_from_slice(&pixels[..copy_len]);
711 service.atlas.dirty = false;
712 }
713 }
714
715 pub fn layout_single_line(
725 &mut self,
726 service: &mut TextFontService,
727 text: &str,
728 format: &TextFormat,
729 max_width: Option<f32>,
730 ) -> SingleLineResult {
731 let empty = SingleLineResult {
732 width: 0.0,
733 height: 0.0,
734 baseline: 0.0,
735 underline_offset: 0.0,
736 underline_thickness: 0.0,
737 glyphs: Vec::new(),
738 glyph_keys: Vec::new(),
739 spans: Vec::new(),
740 };
741
742 if text.is_empty() {
743 return empty;
744 }
745
746 let font_point_size = format.font_size.map(|s| s as u32);
747 let resolved = match resolve_font(
748 &service.font_registry,
749 format.font_family.as_deref(),
750 format.font_weight,
751 format.font_bold,
752 format.font_italic,
753 font_point_size,
754 service.scale_factor,
755 ) {
756 Some(r) => r,
757 None => return empty,
758 };
759
760 let metrics = match font_metrics_px(&service.font_registry, &resolved) {
761 Some(m) => m,
762 None => return empty,
763 };
764 let line_height = metrics.ascent + metrics.descent + metrics.leading;
765 let baseline = metrics.ascent;
766
767 let runs: Vec<_> = bidi_runs(text)
768 .into_iter()
769 .filter_map(|br| {
770 let slice = text.get(br.byte_range.clone())?;
771 shape_text_with_fallback(
772 &service.font_registry,
773 &resolved,
774 slice,
775 br.byte_range.start,
776 br.direction,
777 )
778 })
779 .collect();
780
781 if runs.is_empty() {
782 return empty;
783 }
784
785 let total_advance: f32 = runs.iter().map(|r| r.advance_width).sum();
786
787 let (truncate_at_visual_index, final_width, ellipsis_run) = if let Some(max_w) = max_width
788 && total_advance > max_w
789 {
790 let ellipsis_run = shape_text(&service.font_registry, &resolved, "\u{2026}", 0);
791 let ellipsis_width = ellipsis_run
792 .as_ref()
793 .map(|r| r.advance_width)
794 .unwrap_or(0.0);
795 let budget = (max_w - ellipsis_width).max(0.0);
796
797 let mut used = 0.0f32;
798 let mut count = 0usize;
799 'outer: for run in &runs {
800 for g in &run.glyphs {
801 if used + g.x_advance > budget {
802 break 'outer;
803 }
804 used += g.x_advance;
805 count += 1;
806 }
807 }
808
809 (Some(count), used + ellipsis_width, ellipsis_run)
810 } else {
811 (None, total_advance, None)
812 };
813
814 let text_color = format.color.unwrap_or(self.text_color);
815 let glyph_capacity: usize = runs.iter().map(|r| r.glyphs.len()).sum();
816 let mut quads = Vec::with_capacity(glyph_capacity + 1);
817 let mut keys = Vec::with_capacity(glyph_capacity + 1);
818 let mut pen_x = 0.0f32;
819 let mut emitted = 0usize;
820
821 'emit: for run in &runs {
822 for glyph in &run.glyphs {
823 if let Some(limit) = truncate_at_visual_index
824 && emitted >= limit
825 {
826 break 'emit;
827 }
828 rasterize_glyph_quad(
829 service, glyph, run, pen_x, baseline, text_color, &mut quads, &mut keys,
830 );
831 pen_x += glyph.x_advance;
832 emitted += 1;
833 }
834 }
835
836 if let Some(ref e_run) = ellipsis_run {
837 for glyph in &e_run.glyphs {
838 rasterize_glyph_quad(
839 service, glyph, e_run, pen_x, baseline, text_color, &mut quads, &mut keys,
840 );
841 pen_x += glyph.x_advance;
842 }
843 }
844
845 SingleLineResult {
846 width: final_width,
847 height: line_height,
848 baseline,
849 underline_offset: metrics.underline_offset,
850 underline_thickness: metrics.stroke_size,
851 glyphs: quads,
852 glyph_keys: keys,
853 spans: Vec::new(),
854 }
855 }
856
857 pub fn layout_paragraph(
868 &mut self,
869 service: &mut TextFontService,
870 text: &str,
871 format: &TextFormat,
872 max_width: f32,
873 max_lines: Option<usize>,
874 ) -> ParagraphResult {
875 let empty = ParagraphResult {
876 width: 0.0,
877 height: 0.0,
878 baseline_first: 0.0,
879 line_count: 0,
880 line_height: 0.0,
881 underline_offset: 0.0,
882 underline_thickness: 0.0,
883 glyphs: Vec::new(),
884 glyph_keys: Vec::new(),
885 spans: Vec::new(),
886 };
887
888 if text.is_empty() || max_width <= 0.0 {
889 return empty;
890 }
891
892 let font_point_size = format.font_size.map(|s| s as u32);
893 let resolved = match resolve_font(
894 &service.font_registry,
895 format.font_family.as_deref(),
896 format.font_weight,
897 format.font_bold,
898 format.font_italic,
899 font_point_size,
900 service.scale_factor,
901 ) {
902 Some(r) => r,
903 None => return empty,
904 };
905
906 let metrics = match font_metrics_px(&service.font_registry, &resolved) {
907 Some(m) => m,
908 None => return empty,
909 };
910
911 let runs: Vec<_> = bidi_runs(text)
912 .into_iter()
913 .filter_map(|br| {
914 let slice = text.get(br.byte_range.clone())?;
915 shape_text_with_fallback(
916 &service.font_registry,
917 &resolved,
918 slice,
919 br.byte_range.start,
920 br.direction,
921 )
922 })
923 .collect();
924
925 if runs.is_empty() {
926 return empty;
927 }
928
929 let lines = break_into_lines(runs, text, max_width, Alignment::Left, 0.0, &metrics);
930
931 let line_count = match max_lines {
932 Some(n) => lines.len().min(n),
933 None => lines.len(),
934 };
935
936 let text_color = format.color.unwrap_or(self.text_color);
937 let mut quads: Vec<GlyphQuad> = Vec::new();
938 let mut keys: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
939 let mut y_top = 0.0f32;
940 let mut max_line_width = 0.0f32;
941 let baseline_first = metrics.ascent;
942
943 for line in lines.iter().take(line_count) {
944 if line.width > max_line_width {
945 max_line_width = line.width;
946 }
947 let baseline_y = y_top + metrics.ascent;
948 for run in &line.runs {
949 let mut pen_x = run.x;
950 let run_copy = run.shaped_run.clone();
951 for glyph in &run_copy.glyphs {
952 rasterize_glyph_quad(
953 service, glyph, &run_copy, pen_x, baseline_y, text_color, &mut quads,
954 &mut keys,
955 );
956 pen_x += glyph.x_advance;
957 }
958 }
959 y_top += metrics.ascent + metrics.descent + metrics.leading;
960 }
961
962 let line_height = metrics.ascent + metrics.descent + metrics.leading;
963 ParagraphResult {
964 width: max_line_width,
965 height: y_top,
966 baseline_first,
967 line_count,
968 line_height,
969 underline_offset: metrics.underline_offset,
970 underline_thickness: metrics.stroke_size,
971 glyphs: quads,
972 glyph_keys: keys,
973 spans: Vec::new(),
974 }
975 }
976
977 pub fn layout_single_line_markup(
983 &mut self,
984 service: &mut TextFontService,
985 markup: &InlineMarkup,
986 format: &TextFormat,
987 max_width: Option<f32>,
988 ) -> SingleLineResult {
989 if markup.spans.is_empty() {
990 return SingleLineResult {
991 width: 0.0,
992 height: 0.0,
993 baseline: 0.0,
994 underline_offset: 0.0,
995 underline_thickness: 0.0,
996 glyphs: Vec::new(),
997 glyph_keys: Vec::new(),
998 spans: Vec::new(),
999 };
1000 }
1001
1002 let per_span: Vec<(SingleLineResult, &crate::layout::inline_markup::InlineSpan)> = markup
1003 .spans
1004 .iter()
1005 .map(|sp| {
1006 let fmt = merge_format(format, sp.attrs);
1007 let r = if sp.text.is_empty() {
1008 SingleLineResult {
1009 width: 0.0,
1010 height: 0.0,
1011 baseline: 0.0,
1012 underline_offset: 0.0,
1013 underline_thickness: 0.0,
1014 glyphs: Vec::new(),
1015 glyph_keys: Vec::new(),
1016 spans: Vec::new(),
1017 }
1018 } else {
1019 self.layout_single_line(service, &sp.text, &fmt, None)
1020 };
1021 (r, sp)
1022 })
1023 .collect();
1024
1025 let total_width: f32 = per_span.iter().map(|(r, _)| r.width).sum();
1026 let line_height = per_span
1027 .iter()
1028 .map(|(r, _)| r.height)
1029 .fold(0.0f32, f32::max);
1030 let baseline = per_span
1031 .iter()
1032 .map(|(r, _)| r.baseline)
1033 .fold(0.0f32, f32::max);
1034 let (underline_offset, underline_thickness) = per_span
1038 .iter()
1039 .map(|(r, _)| (r.underline_offset, r.underline_thickness))
1040 .find(|(_, t)| *t > 0.0)
1041 .unwrap_or((0.0, 0.0));
1042
1043 let truncate = match max_width {
1044 Some(mw) if total_width > mw => Some(mw),
1045 _ => None,
1046 };
1047
1048 let mut glyphs: Vec<GlyphQuad> = Vec::new();
1049 let mut all_keys: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
1050 let mut spans_out: Vec<LaidOutSpan> = Vec::new();
1051 let mut pen_x: f32 = 0.0;
1052 let effective_width = truncate.unwrap_or(total_width);
1053
1054 for (r, sp) in &per_span {
1055 let remaining = (effective_width - pen_x).max(0.0);
1056 let span_visible_width = r.width.min(remaining);
1057 if span_visible_width <= 0.0 && r.width > 0.0 {
1058 spans_out.push(LaidOutSpan {
1059 kind: if let Some(url) = sp.link_url.clone() {
1060 LaidOutSpanKind::Link { url }
1061 } else {
1062 LaidOutSpanKind::Text
1063 },
1064 line_index: 0,
1065 rect: [pen_x, 0.0, 0.0, line_height],
1066 byte_range: sp.byte_range.clone(),
1067 });
1068 continue;
1069 }
1070
1071 for (gi, g) in r.glyphs.iter().enumerate() {
1072 let g_right = pen_x + g.screen[0] + g.screen[2];
1073 if g_right > effective_width + 0.5 {
1074 break;
1075 }
1076 let mut gq = g.clone();
1077 gq.screen[0] += pen_x;
1078 glyphs.push(gq);
1079 if let Some(k) = r.glyph_keys.get(gi) {
1080 all_keys.push(*k);
1081 }
1082 }
1083
1084 spans_out.push(LaidOutSpan {
1085 kind: if let Some(url) = sp.link_url.clone() {
1086 LaidOutSpanKind::Link { url }
1087 } else {
1088 LaidOutSpanKind::Text
1089 },
1090 line_index: 0,
1091 rect: [pen_x, 0.0, span_visible_width, line_height],
1092 byte_range: sp.byte_range.clone(),
1093 });
1094
1095 pen_x += r.width;
1096 if truncate.is_some() && pen_x >= effective_width {
1097 break;
1098 }
1099 }
1100
1101 SingleLineResult {
1102 width: effective_width,
1103 height: line_height,
1104 baseline,
1105 underline_offset,
1106 underline_thickness,
1107 glyphs,
1108 glyph_keys: all_keys,
1109 spans: spans_out,
1110 }
1111 }
1112
1113 pub fn layout_paragraph_markup(
1118 &mut self,
1119 service: &mut TextFontService,
1120 markup: &InlineMarkup,
1121 format: &TextFormat,
1122 max_width: f32,
1123 max_lines: Option<usize>,
1124 ) -> ParagraphResult {
1125 let empty = ParagraphResult {
1126 width: 0.0,
1127 height: 0.0,
1128 baseline_first: 0.0,
1129 line_count: 0,
1130 line_height: 0.0,
1131 underline_offset: 0.0,
1132 underline_thickness: 0.0,
1133 glyphs: Vec::new(),
1134 glyph_keys: Vec::new(),
1135 spans: Vec::new(),
1136 };
1137
1138 if markup.spans.is_empty() || max_width <= 0.0 {
1139 return empty;
1140 }
1141
1142 let mut flat = String::new();
1143 let mut span_flat_offsets: Vec<usize> = Vec::with_capacity(markup.spans.len());
1144 for sp in &markup.spans {
1145 span_flat_offsets.push(flat.len());
1146 flat.push_str(&sp.text);
1147 }
1148 if flat.is_empty() {
1149 return empty;
1150 }
1151
1152 let base_point_size = format.font_size.map(|s| s as u32);
1153 let base_resolved = match resolve_font(
1154 &service.font_registry,
1155 format.font_family.as_deref(),
1156 format.font_weight,
1157 format.font_bold,
1158 format.font_italic,
1159 base_point_size,
1160 service.scale_factor,
1161 ) {
1162 Some(r) => r,
1163 None => return empty,
1164 };
1165 let metrics = match font_metrics_px(&service.font_registry, &base_resolved) {
1166 Some(m) => m,
1167 None => return empty,
1168 };
1169
1170 let mut all_runs: Vec<ShapedRun> = Vec::new();
1171 for (span_idx, sp) in markup.spans.iter().enumerate() {
1172 if sp.text.is_empty() {
1173 continue;
1174 }
1175 let fmt = merge_format(format, sp.attrs);
1176 let span_point_size = fmt.font_size.map(|s| s as u32);
1177 let Some(resolved) = resolve_font(
1178 &service.font_registry,
1179 fmt.font_family.as_deref(),
1180 fmt.font_weight,
1181 fmt.font_bold,
1182 fmt.font_italic,
1183 span_point_size,
1184 service.scale_factor,
1185 ) else {
1186 continue;
1187 };
1188
1189 let flat_start = span_flat_offsets[span_idx];
1190 for br in bidi_runs(&sp.text) {
1191 let slice = match sp.text.get(br.byte_range.clone()) {
1192 Some(s) => s,
1193 None => continue,
1194 };
1195 let Some(mut run) = shape_text_with_fallback(
1196 &service.font_registry,
1197 &resolved,
1198 slice,
1199 flat_start + br.byte_range.start,
1200 br.direction,
1201 ) else {
1202 continue;
1203 };
1204 if let Some(url) = sp.link_url.as_ref() {
1205 run.is_link = true;
1206 run.anchor_href = Some(url.clone());
1207 }
1208 all_runs.push(run);
1209 }
1210 }
1211
1212 if all_runs.is_empty() {
1213 return empty;
1214 }
1215
1216 let lines = break_into_lines(all_runs, &flat, max_width, Alignment::Left, 0.0, &metrics);
1217
1218 let line_count = match max_lines {
1219 Some(n) => lines.len().min(n),
1220 None => lines.len(),
1221 };
1222
1223 let text_color = format.color.unwrap_or(self.text_color);
1224 let mut glyphs_out: Vec<GlyphQuad> = Vec::new();
1225 let mut keys_out: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
1226 let mut spans_out: Vec<LaidOutSpan> = Vec::new();
1227 let line_height = metrics.ascent + metrics.descent + metrics.leading;
1228 let mut y_top: f32 = 0.0;
1229 let mut max_line_width: f32 = 0.0;
1230 let baseline_first = metrics.ascent;
1231
1232 for (line_idx, line) in lines.iter().take(line_count).enumerate() {
1233 if line.width > max_line_width {
1234 max_line_width = line.width;
1235 }
1236 let baseline_y = y_top + metrics.ascent;
1237
1238 for pr in &line.runs {
1239 let run_copy = pr.shaped_run.clone();
1240 let mut pen_x = pr.x;
1241 for glyph in &run_copy.glyphs {
1242 rasterize_glyph_quad(
1243 service,
1244 glyph,
1245 &run_copy,
1246 pen_x,
1247 baseline_y,
1248 text_color,
1249 &mut glyphs_out,
1250 &mut keys_out,
1251 );
1252 pen_x += glyph.x_advance;
1253 }
1254
1255 if pr.decorations.is_link
1256 && let Some(url) = pr.decorations.anchor_href.clone()
1257 {
1258 let width = pr.shaped_run.advance_width;
1259 spans_out.push(LaidOutSpan {
1260 kind: LaidOutSpanKind::Link { url },
1261 line_index: line_idx,
1262 rect: [pr.x, y_top, width, line_height],
1263 byte_range: pr.shaped_run.text_range.clone(),
1264 });
1265 }
1266 }
1267
1268 y_top += line_height;
1269 }
1270
1271 ParagraphResult {
1272 width: max_line_width,
1273 height: y_top,
1274 baseline_first,
1275 line_count,
1276 line_height,
1277 underline_offset: metrics.underline_offset,
1278 underline_thickness: metrics.stroke_size,
1279 glyphs: glyphs_out,
1280 glyph_keys: keys_out,
1281 spans: spans_out,
1282 }
1283 }
1284
1285 pub fn hit_test(&self, x: f32, y: f32) -> Option<HitTestResult> {
1292 crate::render::hit_test::hit_test(
1293 &self.flow_layout,
1294 self.scroll_offset,
1295 x / self.zoom,
1296 y / self.zoom,
1297 )
1298 }
1299
1300 pub fn character_geometry(
1309 &self,
1310 block_id: usize,
1311 char_start: usize,
1312 char_end: usize,
1313 ) -> Vec<CharacterGeometry> {
1314 if char_start >= char_end {
1315 return Vec::new();
1316 }
1317 let block = match self.flow_layout.blocks.get(&block_id) {
1318 Some(b) => b,
1319 None => return Vec::new(),
1320 };
1321
1322 let mut absolute: Vec<(usize, f32)> = Vec::with_capacity(char_end - char_start);
1323 for line in &block.lines {
1324 if line.char_range.end <= char_start || line.char_range.start >= char_end {
1325 continue;
1326 }
1327 let local_start = char_start.max(line.char_range.start);
1328 let local_end = char_end.min(line.char_range.end);
1329 for c in local_start..local_end {
1330 let x = line.x_for_offset(c);
1331 absolute.push((c, x));
1332 }
1333 if local_end == char_end {
1334 let x_end = line.x_for_offset(local_end);
1335 absolute.push((local_end, x_end));
1336 }
1337 }
1338
1339 if absolute.is_empty() {
1340 return Vec::new();
1341 }
1342
1343 absolute.sort_by_key(|(c, _)| *c);
1344
1345 let base_x = absolute.first().map(|(_, x)| *x).unwrap_or(0.0);
1346 let mut out: Vec<CharacterGeometry> = Vec::with_capacity(absolute.len());
1347 for window in absolute.windows(2) {
1348 let (c, x) = window[0];
1349 let (_, x_next) = window[1];
1350 if c >= char_end {
1351 break;
1352 }
1353 out.push(CharacterGeometry {
1354 position: x - base_x,
1355 width: (x_next - x).max(0.0),
1356 });
1357 }
1358 out
1359 }
1360
1361 pub fn caret_rect(&self, position: usize) -> [f32; 4] {
1367 let mut rect =
1368 crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position);
1369 rect[0] *= self.zoom;
1370 rect[1] *= self.zoom;
1371 rect[2] *= self.zoom;
1372 rect[3] *= self.zoom;
1373 rect
1374 }
1375
1376 pub fn set_cursor(&mut self, cursor: &CursorDisplay) {
1380 self.cursors = vec![CursorDisplay {
1381 position: cursor.position,
1382 anchor: cursor.anchor,
1383 visible: cursor.visible,
1384 selected_cells: cursor.selected_cells.clone(),
1385 }];
1386 }
1387
1388 pub fn set_cursors(&mut self, cursors: &[CursorDisplay]) {
1392 self.cursors = cursors
1393 .iter()
1394 .map(|c| CursorDisplay {
1395 position: c.position,
1396 anchor: c.anchor,
1397 visible: c.visible,
1398 selected_cells: c.selected_cells.clone(),
1399 })
1400 .collect();
1401 }
1402
1403 pub fn set_selection_color(&mut self, color: [f32; 4]) {
1406 self.selection_color = color;
1407 }
1408
1409 pub fn set_cursor_color(&mut self, color: [f32; 4]) {
1411 self.cursor_color = color;
1412 }
1413
1414 pub fn set_text_color(&mut self, color: [f32; 4]) {
1417 self.text_color = color;
1418 }
1419
1420 pub fn text_color(&self) -> [f32; 4] {
1422 self.text_color
1423 }
1424
1425 pub fn block_visual_info(&self, block_id: usize) -> Option<BlockVisualInfo> {
1430 let block = self.flow_layout.blocks.get(&block_id)?;
1431 Some(BlockVisualInfo {
1432 block_id,
1433 y: block.y,
1434 height: block.height,
1435 })
1436 }
1437
1438 pub fn is_block_in_table(&self, block_id: usize) -> bool {
1440 self.flow_layout.tables.values().any(|table| {
1441 table
1442 .cell_layouts
1443 .iter()
1444 .any(|cell| cell.blocks.iter().any(|b| b.block_id == block_id))
1445 })
1446 }
1447
1448 pub fn scroll_to_position(&mut self, position: usize) -> f32 {
1451 let rect =
1452 crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position);
1453 let target_y = rect[1] + self.scroll_offset - self.viewport_height / (3.0 * self.zoom);
1454 self.scroll_offset = target_y.max(0.0);
1455 self.scroll_offset
1456 }
1457
1458 pub fn ensure_caret_visible(&mut self) -> Option<f32> {
1462 if self.cursors.is_empty() {
1463 return None;
1464 }
1465 let pos = self.cursors[0].position;
1466 let rect = crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, pos);
1467 let caret_screen_y = rect[1];
1468 let caret_screen_bottom = caret_screen_y + rect[3];
1469 let effective_vh = self.viewport_height / self.zoom;
1470 let margin = 10.0 / self.zoom;
1471 let old_offset = self.scroll_offset;
1472
1473 if caret_screen_y < 0.0 {
1474 self.scroll_offset += caret_screen_y - margin;
1475 self.scroll_offset = self.scroll_offset.max(0.0);
1476 } else if caret_screen_bottom > effective_vh {
1477 self.scroll_offset += caret_screen_bottom - effective_vh + margin;
1478 }
1479
1480 if (self.scroll_offset - old_offset).abs() > 0.001 {
1481 Some(self.scroll_offset)
1482 } else {
1483 None
1484 }
1485 }
1486}
1487
1488impl Default for DocumentFlow {
1489 fn default() -> Self {
1490 Self::new()
1491 }
1492}
1493
1494#[cfg(feature = "text-document")]
1495enum FlowItemKind {
1496 Block(BlockLayoutParams),
1497 Table(TableLayoutParams),
1498 Frame(FrameLayoutParams),
1499}
1500
1501#[allow(clippy::too_many_arguments)]
1506fn rasterize_glyph_quad(
1507 service: &mut TextFontService,
1508 glyph: &ShapedGlyph,
1509 run: &ShapedRun,
1510 pen_x: f32,
1511 baseline: f32,
1512 text_color: [f32; 4],
1513 quads: &mut Vec<GlyphQuad>,
1514 glyph_keys: &mut Vec<crate::atlas::cache::GlyphCacheKey>,
1515) {
1516 use crate::atlas::cache::GlyphCacheKey;
1517 use crate::atlas::rasterizer::rasterize_glyph;
1518
1519 if glyph.glyph_id == 0 {
1520 return;
1521 }
1522
1523 let entry = match service.font_registry.get(glyph.font_face_id) {
1524 Some(e) => e,
1525 None => return,
1526 };
1527
1528 let sf = service.scale_factor.max(f32::MIN_POSITIVE);
1529 let inv_sf = 1.0 / sf;
1530 let physical_size_px = run.size_px * sf;
1531 let cache_key = GlyphCacheKey::with_weight(
1532 glyph.font_face_id,
1533 glyph.glyph_id,
1534 physical_size_px,
1535 run.weight as u32,
1536 );
1537
1538 if service.glyph_cache.peek(&cache_key).is_none()
1539 && let Some(image) = rasterize_glyph(
1540 &mut service.scale_context,
1541 &entry.data,
1542 entry.face_index,
1543 entry.swash_cache_key,
1544 glyph.glyph_id,
1545 physical_size_px,
1546 run.weight as u32,
1547 )
1548 && image.width > 0
1549 && image.height > 0
1550 && let Some(alloc) = service.atlas.allocate(image.width, image.height)
1551 {
1552 let rect = alloc.rectangle;
1553 let atlas_x = rect.min.x as u32;
1554 let atlas_y = rect.min.y as u32;
1555 if image.is_color {
1556 service
1557 .atlas
1558 .blit_rgba(atlas_x, atlas_y, image.width, image.height, &image.data);
1559 } else {
1560 service
1561 .atlas
1562 .blit_mask(atlas_x, atlas_y, image.width, image.height, &image.data);
1563 }
1564 service.glyph_cache.insert(
1565 cache_key,
1566 crate::atlas::cache::CachedGlyph {
1567 alloc_id: alloc.id,
1568 atlas_x,
1569 atlas_y,
1570 width: image.width,
1571 height: image.height,
1572 placement_left: image.placement_left,
1573 placement_top: image.placement_top,
1574 is_color: image.is_color,
1575 last_used: 0,
1576 },
1577 );
1578 }
1579
1580 if let Some(cached) = service.glyph_cache.get(&cache_key) {
1581 let logical_w = cached.width as f32 * inv_sf;
1582 let logical_h = cached.height as f32 * inv_sf;
1583 let logical_left = cached.placement_left as f32 * inv_sf;
1584 let logical_top = cached.placement_top as f32 * inv_sf;
1585 let screen_x = pen_x + glyph.x_offset + logical_left;
1586 let screen_y = baseline - glyph.y_offset - logical_top;
1587 let color = if cached.is_color {
1588 [1.0, 1.0, 1.0, 1.0]
1589 } else {
1590 text_color
1591 };
1592 quads.push(GlyphQuad {
1593 screen: [screen_x, screen_y, logical_w, logical_h],
1594 atlas: [
1595 cached.atlas_x as f32,
1596 cached.atlas_y as f32,
1597 cached.width as f32,
1598 cached.height as f32,
1599 ],
1600 color,
1601 is_color: cached.is_color,
1602 });
1603 glyph_keys.push(cache_key);
1604 }
1605}
1606
1607fn apply_zoom(frame: &mut RenderFrame, zoom: f32) {
1609 if (zoom - 1.0).abs() <= f32::EPSILON {
1610 return;
1611 }
1612 for q in &mut frame.glyphs {
1613 q.screen[0] *= zoom;
1614 q.screen[1] *= zoom;
1615 q.screen[2] *= zoom;
1616 q.screen[3] *= zoom;
1617 }
1618 for q in &mut frame.images {
1619 q.screen[0] *= zoom;
1620 q.screen[1] *= zoom;
1621 q.screen[2] *= zoom;
1622 q.screen[3] *= zoom;
1623 }
1624 apply_zoom_decorations(&mut frame.decorations, zoom);
1625}
1626
1627fn apply_zoom_decorations(decorations: &mut [DecorationRect], zoom: f32) {
1629 if (zoom - 1.0).abs() <= f32::EPSILON {
1630 return;
1631 }
1632 for d in decorations.iter_mut() {
1633 d.rect[0] *= zoom;
1634 d.rect[1] *= zoom;
1635 d.rect[2] *= zoom;
1636 d.rect[3] *= zoom;
1637 }
1638}
1639
1640fn merge_format(base: &TextFormat, attrs: InlineAttrs) -> TextFormat {
1643 let mut fmt = base.clone();
1644 if attrs.is_bold() {
1645 fmt.font_bold = Some(true);
1646 if let Some(w) = fmt.font_weight
1647 && w < 600
1648 {
1649 fmt.font_weight = Some(700);
1650 } else if fmt.font_weight.is_none() {
1651 fmt.font_weight = Some(700);
1652 }
1653 }
1654 if attrs.is_italic() {
1655 fmt.font_italic = Some(true);
1656 }
1657 fmt
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662 use super::*;
1663 use crate::layout::block::{BlockLayoutParams, FragmentParams};
1664 use crate::layout::paragraph::Alignment;
1665 use crate::types::{UnderlineStyle, VerticalAlignment};
1666
1667 const NOTO_SANS: &[u8] = include_bytes!("../test-fonts/NotoSans-Variable.ttf");
1668
1669 fn service() -> TextFontService {
1670 let mut s = TextFontService::new();
1671 let face = s.register_font(NOTO_SANS);
1672 s.set_default_font(face, 16.0);
1673 s
1674 }
1675
1676 fn block(id: usize, text: &str) -> BlockLayoutParams {
1677 BlockLayoutParams {
1678 block_id: id,
1679 position: 0,
1680 text: text.to_string(),
1681 fragments: vec![FragmentParams {
1682 text: text.to_string(),
1683 offset: 0,
1684 length: text.len(),
1685 font_family: None,
1686 font_weight: None,
1687 font_bold: None,
1688 font_italic: None,
1689 font_point_size: None,
1690 underline_style: UnderlineStyle::None,
1691 overline: false,
1692 strikeout: false,
1693 is_link: false,
1694 letter_spacing: 0.0,
1695 word_spacing: 0.0,
1696 foreground_color: None,
1697 underline_color: None,
1698 background_color: None,
1699 anchor_href: None,
1700 tooltip: None,
1701 vertical_alignment: VerticalAlignment::Normal,
1702 image_name: None,
1703 image_width: 0.0,
1704 image_height: 0.0,
1705 }],
1706 alignment: Alignment::Left,
1707 top_margin: 0.0,
1708 bottom_margin: 0.0,
1709 left_margin: 0.0,
1710 right_margin: 0.0,
1711 text_indent: 0.0,
1712 list_marker: String::new(),
1713 list_indent: 0.0,
1714 tab_positions: vec![],
1715 line_height_multiplier: None,
1716 non_breakable_lines: false,
1717 checkbox: None,
1718 background_color: None,
1719 }
1720 }
1721
1722 #[test]
1723 fn relayout_block_returns_no_layout_when_never_laid_out() {
1724 let svc = service();
1725 let mut flow = DocumentFlow::new();
1726 flow.set_viewport(400.0, 200.0);
1727 let err = flow.relayout_block(&svc, &block(1, "Hello")).unwrap_err();
1728 assert_eq!(err, RelayoutError::NoLayout);
1729 }
1730
1731 #[test]
1732 fn relayout_block_returns_scale_dirty_after_scale_factor_change() {
1733 let mut svc = service();
1734 let mut flow = DocumentFlow::new();
1735 flow.set_viewport(400.0, 200.0);
1736 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
1737 assert!(flow.has_layout());
1738
1739 svc.set_scale_factor(2.0);
1741 assert!(flow.layout_dirty_for_scale(&svc));
1742
1743 let err = flow
1744 .relayout_block(&svc, &block(1, "Hello world"))
1745 .unwrap_err();
1746 assert_eq!(err, RelayoutError::ScaleDirty);
1747 }
1748
1749 #[test]
1750 fn relayout_block_succeeds_after_fresh_layout_post_scale_change() {
1751 let mut svc = service();
1752 let mut flow = DocumentFlow::new();
1753 flow.set_viewport(400.0, 200.0);
1754 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
1755
1756 svc.set_scale_factor(2.0);
1757 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
1760 assert!(!flow.layout_dirty_for_scale(&svc));
1761
1762 flow.relayout_block(&svc, &block(1, "Hello world"))
1764 .expect("relayout_block must succeed after a fresh post-scale layout");
1765 }
1766}