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 glyphs: Vec::new(),
736 spans: Vec::new(),
737 };
738
739 if text.is_empty() {
740 return empty;
741 }
742
743 let font_point_size = format.font_size.map(|s| s as u32);
744 let resolved = match resolve_font(
745 &service.font_registry,
746 format.font_family.as_deref(),
747 format.font_weight,
748 format.font_bold,
749 format.font_italic,
750 font_point_size,
751 service.scale_factor,
752 ) {
753 Some(r) => r,
754 None => return empty,
755 };
756
757 let metrics = match font_metrics_px(&service.font_registry, &resolved) {
758 Some(m) => m,
759 None => return empty,
760 };
761 let line_height = metrics.ascent + metrics.descent + metrics.leading;
762 let baseline = metrics.ascent;
763
764 let runs: Vec<_> = bidi_runs(text)
765 .into_iter()
766 .filter_map(|br| {
767 let slice = text.get(br.byte_range.clone())?;
768 shape_text_with_fallback(
769 &service.font_registry,
770 &resolved,
771 slice,
772 br.byte_range.start,
773 br.direction,
774 )
775 })
776 .collect();
777
778 if runs.is_empty() {
779 return empty;
780 }
781
782 let total_advance: f32 = runs.iter().map(|r| r.advance_width).sum();
783
784 let (truncate_at_visual_index, final_width, ellipsis_run) = if let Some(max_w) = max_width
785 && total_advance > max_w
786 {
787 let ellipsis_run = shape_text(&service.font_registry, &resolved, "\u{2026}", 0);
788 let ellipsis_width = ellipsis_run
789 .as_ref()
790 .map(|r| r.advance_width)
791 .unwrap_or(0.0);
792 let budget = (max_w - ellipsis_width).max(0.0);
793
794 let mut used = 0.0f32;
795 let mut count = 0usize;
796 'outer: for run in &runs {
797 for g in &run.glyphs {
798 if used + g.x_advance > budget {
799 break 'outer;
800 }
801 used += g.x_advance;
802 count += 1;
803 }
804 }
805
806 (Some(count), used + ellipsis_width, ellipsis_run)
807 } else {
808 (None, total_advance, None)
809 };
810
811 let text_color = format.color.unwrap_or(self.text_color);
812 let glyph_capacity: usize = runs.iter().map(|r| r.glyphs.len()).sum();
813 let mut quads = Vec::with_capacity(glyph_capacity + 1);
814 let mut pen_x = 0.0f32;
815 let mut emitted = 0usize;
816
817 'emit: for run in &runs {
818 for glyph in &run.glyphs {
819 if let Some(limit) = truncate_at_visual_index
820 && emitted >= limit
821 {
822 break 'emit;
823 }
824 rasterize_glyph_quad(service, glyph, run, pen_x, baseline, text_color, &mut quads);
825 pen_x += glyph.x_advance;
826 emitted += 1;
827 }
828 }
829
830 if let Some(ref e_run) = ellipsis_run {
831 for glyph in &e_run.glyphs {
832 rasterize_glyph_quad(
833 service, glyph, e_run, pen_x, baseline, text_color, &mut quads,
834 );
835 pen_x += glyph.x_advance;
836 }
837 }
838
839 SingleLineResult {
840 width: final_width,
841 height: line_height,
842 baseline,
843 glyphs: quads,
844 spans: Vec::new(),
845 }
846 }
847
848 pub fn layout_paragraph(
859 &mut self,
860 service: &mut TextFontService,
861 text: &str,
862 format: &TextFormat,
863 max_width: f32,
864 max_lines: Option<usize>,
865 ) -> ParagraphResult {
866 let empty = ParagraphResult {
867 width: 0.0,
868 height: 0.0,
869 baseline_first: 0.0,
870 line_count: 0,
871 line_height: 0.0,
872 glyphs: Vec::new(),
873 spans: Vec::new(),
874 };
875
876 if text.is_empty() || max_width <= 0.0 {
877 return empty;
878 }
879
880 let font_point_size = format.font_size.map(|s| s as u32);
881 let resolved = match resolve_font(
882 &service.font_registry,
883 format.font_family.as_deref(),
884 format.font_weight,
885 format.font_bold,
886 format.font_italic,
887 font_point_size,
888 service.scale_factor,
889 ) {
890 Some(r) => r,
891 None => return empty,
892 };
893
894 let metrics = match font_metrics_px(&service.font_registry, &resolved) {
895 Some(m) => m,
896 None => return empty,
897 };
898
899 let runs: Vec<_> = bidi_runs(text)
900 .into_iter()
901 .filter_map(|br| {
902 let slice = text.get(br.byte_range.clone())?;
903 shape_text_with_fallback(
904 &service.font_registry,
905 &resolved,
906 slice,
907 br.byte_range.start,
908 br.direction,
909 )
910 })
911 .collect();
912
913 if runs.is_empty() {
914 return empty;
915 }
916
917 let lines = break_into_lines(runs, text, max_width, Alignment::Left, 0.0, &metrics);
918
919 let line_count = match max_lines {
920 Some(n) => lines.len().min(n),
921 None => lines.len(),
922 };
923
924 let text_color = format.color.unwrap_or(self.text_color);
925 let mut quads: Vec<GlyphQuad> = Vec::new();
926 let mut y_top = 0.0f32;
927 let mut max_line_width = 0.0f32;
928 let baseline_first = metrics.ascent;
929
930 for line in lines.iter().take(line_count) {
931 if line.width > max_line_width {
932 max_line_width = line.width;
933 }
934 let baseline_y = y_top + metrics.ascent;
935 for run in &line.runs {
936 let mut pen_x = run.x;
937 let run_copy = run.shaped_run.clone();
938 for glyph in &run_copy.glyphs {
939 rasterize_glyph_quad(
940 service, glyph, &run_copy, pen_x, baseline_y, text_color, &mut quads,
941 );
942 pen_x += glyph.x_advance;
943 }
944 }
945 y_top += metrics.ascent + metrics.descent + metrics.leading;
946 }
947
948 let line_height = metrics.ascent + metrics.descent + metrics.leading;
949 ParagraphResult {
950 width: max_line_width,
951 height: y_top,
952 baseline_first,
953 line_count,
954 line_height,
955 glyphs: quads,
956 spans: Vec::new(),
957 }
958 }
959
960 pub fn layout_single_line_markup(
966 &mut self,
967 service: &mut TextFontService,
968 markup: &InlineMarkup,
969 format: &TextFormat,
970 max_width: Option<f32>,
971 ) -> SingleLineResult {
972 if markup.spans.is_empty() {
973 return SingleLineResult {
974 width: 0.0,
975 height: 0.0,
976 baseline: 0.0,
977 glyphs: Vec::new(),
978 spans: Vec::new(),
979 };
980 }
981
982 let per_span: Vec<(SingleLineResult, &crate::layout::inline_markup::InlineSpan)> = markup
983 .spans
984 .iter()
985 .map(|sp| {
986 let fmt = merge_format(format, sp.attrs);
987 let r = if sp.text.is_empty() {
988 SingleLineResult {
989 width: 0.0,
990 height: 0.0,
991 baseline: 0.0,
992 glyphs: Vec::new(),
993 spans: Vec::new(),
994 }
995 } else {
996 self.layout_single_line(service, &sp.text, &fmt, None)
997 };
998 (r, sp)
999 })
1000 .collect();
1001
1002 let total_width: f32 = per_span.iter().map(|(r, _)| r.width).sum();
1003 let line_height = per_span
1004 .iter()
1005 .map(|(r, _)| r.height)
1006 .fold(0.0f32, f32::max);
1007 let baseline = per_span
1008 .iter()
1009 .map(|(r, _)| r.baseline)
1010 .fold(0.0f32, f32::max);
1011
1012 let truncate = match max_width {
1013 Some(mw) if total_width > mw => Some(mw),
1014 _ => None,
1015 };
1016
1017 let mut glyphs: Vec<GlyphQuad> = Vec::new();
1018 let mut spans_out: Vec<LaidOutSpan> = Vec::new();
1019 let mut pen_x: f32 = 0.0;
1020 let effective_width = truncate.unwrap_or(total_width);
1021
1022 for (r, sp) in &per_span {
1023 let remaining = (effective_width - pen_x).max(0.0);
1024 let span_visible_width = r.width.min(remaining);
1025 if span_visible_width <= 0.0 && r.width > 0.0 {
1026 spans_out.push(LaidOutSpan {
1027 kind: if let Some(url) = sp.link_url.clone() {
1028 LaidOutSpanKind::Link { url }
1029 } else {
1030 LaidOutSpanKind::Text
1031 },
1032 line_index: 0,
1033 rect: [pen_x, 0.0, 0.0, line_height],
1034 byte_range: sp.byte_range.clone(),
1035 });
1036 continue;
1037 }
1038
1039 for g in &r.glyphs {
1040 let g_right = pen_x + g.screen[0] + g.screen[2];
1041 if g_right > effective_width + 0.5 {
1042 break;
1043 }
1044 let mut gq = g.clone();
1045 gq.screen[0] += pen_x;
1046 glyphs.push(gq);
1047 }
1048
1049 spans_out.push(LaidOutSpan {
1050 kind: if let Some(url) = sp.link_url.clone() {
1051 LaidOutSpanKind::Link { url }
1052 } else {
1053 LaidOutSpanKind::Text
1054 },
1055 line_index: 0,
1056 rect: [pen_x, 0.0, span_visible_width, line_height],
1057 byte_range: sp.byte_range.clone(),
1058 });
1059
1060 pen_x += r.width;
1061 if truncate.is_some() && pen_x >= effective_width {
1062 break;
1063 }
1064 }
1065
1066 SingleLineResult {
1067 width: effective_width,
1068 height: line_height,
1069 baseline,
1070 glyphs,
1071 spans: spans_out,
1072 }
1073 }
1074
1075 pub fn layout_paragraph_markup(
1080 &mut self,
1081 service: &mut TextFontService,
1082 markup: &InlineMarkup,
1083 format: &TextFormat,
1084 max_width: f32,
1085 max_lines: Option<usize>,
1086 ) -> ParagraphResult {
1087 let empty = ParagraphResult {
1088 width: 0.0,
1089 height: 0.0,
1090 baseline_first: 0.0,
1091 line_count: 0,
1092 line_height: 0.0,
1093 glyphs: Vec::new(),
1094 spans: Vec::new(),
1095 };
1096
1097 if markup.spans.is_empty() || max_width <= 0.0 {
1098 return empty;
1099 }
1100
1101 let mut flat = String::new();
1102 let mut span_flat_offsets: Vec<usize> = Vec::with_capacity(markup.spans.len());
1103 for sp in &markup.spans {
1104 span_flat_offsets.push(flat.len());
1105 flat.push_str(&sp.text);
1106 }
1107 if flat.is_empty() {
1108 return empty;
1109 }
1110
1111 let base_point_size = format.font_size.map(|s| s as u32);
1112 let base_resolved = match resolve_font(
1113 &service.font_registry,
1114 format.font_family.as_deref(),
1115 format.font_weight,
1116 format.font_bold,
1117 format.font_italic,
1118 base_point_size,
1119 service.scale_factor,
1120 ) {
1121 Some(r) => r,
1122 None => return empty,
1123 };
1124 let metrics = match font_metrics_px(&service.font_registry, &base_resolved) {
1125 Some(m) => m,
1126 None => return empty,
1127 };
1128
1129 let mut all_runs: Vec<ShapedRun> = Vec::new();
1130 for (span_idx, sp) in markup.spans.iter().enumerate() {
1131 if sp.text.is_empty() {
1132 continue;
1133 }
1134 let fmt = merge_format(format, sp.attrs);
1135 let span_point_size = fmt.font_size.map(|s| s as u32);
1136 let Some(resolved) = resolve_font(
1137 &service.font_registry,
1138 fmt.font_family.as_deref(),
1139 fmt.font_weight,
1140 fmt.font_bold,
1141 fmt.font_italic,
1142 span_point_size,
1143 service.scale_factor,
1144 ) else {
1145 continue;
1146 };
1147
1148 let flat_start = span_flat_offsets[span_idx];
1149 for br in bidi_runs(&sp.text) {
1150 let slice = match sp.text.get(br.byte_range.clone()) {
1151 Some(s) => s,
1152 None => continue,
1153 };
1154 let Some(mut run) = shape_text_with_fallback(
1155 &service.font_registry,
1156 &resolved,
1157 slice,
1158 flat_start + br.byte_range.start,
1159 br.direction,
1160 ) else {
1161 continue;
1162 };
1163 if let Some(url) = sp.link_url.as_ref() {
1164 run.is_link = true;
1165 run.anchor_href = Some(url.clone());
1166 }
1167 all_runs.push(run);
1168 }
1169 }
1170
1171 if all_runs.is_empty() {
1172 return empty;
1173 }
1174
1175 let lines = break_into_lines(all_runs, &flat, max_width, Alignment::Left, 0.0, &metrics);
1176
1177 let line_count = match max_lines {
1178 Some(n) => lines.len().min(n),
1179 None => lines.len(),
1180 };
1181
1182 let text_color = format.color.unwrap_or(self.text_color);
1183 let mut glyphs_out: Vec<GlyphQuad> = Vec::new();
1184 let mut spans_out: Vec<LaidOutSpan> = Vec::new();
1185 let line_height = metrics.ascent + metrics.descent + metrics.leading;
1186 let mut y_top: f32 = 0.0;
1187 let mut max_line_width: f32 = 0.0;
1188 let baseline_first = metrics.ascent;
1189
1190 for (line_idx, line) in lines.iter().take(line_count).enumerate() {
1191 if line.width > max_line_width {
1192 max_line_width = line.width;
1193 }
1194 let baseline_y = y_top + metrics.ascent;
1195
1196 for pr in &line.runs {
1197 let run_copy = pr.shaped_run.clone();
1198 let mut pen_x = pr.x;
1199 for glyph in &run_copy.glyphs {
1200 rasterize_glyph_quad(
1201 service,
1202 glyph,
1203 &run_copy,
1204 pen_x,
1205 baseline_y,
1206 text_color,
1207 &mut glyphs_out,
1208 );
1209 pen_x += glyph.x_advance;
1210 }
1211
1212 if pr.decorations.is_link
1213 && let Some(url) = pr.decorations.anchor_href.clone()
1214 {
1215 let width = pr.shaped_run.advance_width;
1216 spans_out.push(LaidOutSpan {
1217 kind: LaidOutSpanKind::Link { url },
1218 line_index: line_idx,
1219 rect: [pr.x, y_top, width, line_height],
1220 byte_range: pr.shaped_run.text_range.clone(),
1221 });
1222 }
1223 }
1224
1225 y_top += line_height;
1226 }
1227
1228 ParagraphResult {
1229 width: max_line_width,
1230 height: y_top,
1231 baseline_first,
1232 line_count,
1233 line_height,
1234 glyphs: glyphs_out,
1235 spans: spans_out,
1236 }
1237 }
1238
1239 pub fn hit_test(&self, x: f32, y: f32) -> Option<HitTestResult> {
1246 crate::render::hit_test::hit_test(
1247 &self.flow_layout,
1248 self.scroll_offset,
1249 x / self.zoom,
1250 y / self.zoom,
1251 )
1252 }
1253
1254 pub fn character_geometry(
1263 &self,
1264 block_id: usize,
1265 char_start: usize,
1266 char_end: usize,
1267 ) -> Vec<CharacterGeometry> {
1268 if char_start >= char_end {
1269 return Vec::new();
1270 }
1271 let block = match self.flow_layout.blocks.get(&block_id) {
1272 Some(b) => b,
1273 None => return Vec::new(),
1274 };
1275
1276 let mut absolute: Vec<(usize, f32)> = Vec::with_capacity(char_end - char_start);
1277 for line in &block.lines {
1278 if line.char_range.end <= char_start || line.char_range.start >= char_end {
1279 continue;
1280 }
1281 let local_start = char_start.max(line.char_range.start);
1282 let local_end = char_end.min(line.char_range.end);
1283 for c in local_start..local_end {
1284 let x = line.x_for_offset(c);
1285 absolute.push((c, x));
1286 }
1287 if local_end == char_end {
1288 let x_end = line.x_for_offset(local_end);
1289 absolute.push((local_end, x_end));
1290 }
1291 }
1292
1293 if absolute.is_empty() {
1294 return Vec::new();
1295 }
1296
1297 absolute.sort_by_key(|(c, _)| *c);
1298
1299 let base_x = absolute.first().map(|(_, x)| *x).unwrap_or(0.0);
1300 let mut out: Vec<CharacterGeometry> = Vec::with_capacity(absolute.len());
1301 for window in absolute.windows(2) {
1302 let (c, x) = window[0];
1303 let (_, x_next) = window[1];
1304 if c >= char_end {
1305 break;
1306 }
1307 out.push(CharacterGeometry {
1308 position: x - base_x,
1309 width: (x_next - x).max(0.0),
1310 });
1311 }
1312 out
1313 }
1314
1315 pub fn caret_rect(&self, position: usize) -> [f32; 4] {
1321 let mut rect =
1322 crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position);
1323 rect[0] *= self.zoom;
1324 rect[1] *= self.zoom;
1325 rect[2] *= self.zoom;
1326 rect[3] *= self.zoom;
1327 rect
1328 }
1329
1330 pub fn set_cursor(&mut self, cursor: &CursorDisplay) {
1334 self.cursors = vec![CursorDisplay {
1335 position: cursor.position,
1336 anchor: cursor.anchor,
1337 visible: cursor.visible,
1338 selected_cells: cursor.selected_cells.clone(),
1339 }];
1340 }
1341
1342 pub fn set_cursors(&mut self, cursors: &[CursorDisplay]) {
1346 self.cursors = cursors
1347 .iter()
1348 .map(|c| CursorDisplay {
1349 position: c.position,
1350 anchor: c.anchor,
1351 visible: c.visible,
1352 selected_cells: c.selected_cells.clone(),
1353 })
1354 .collect();
1355 }
1356
1357 pub fn set_selection_color(&mut self, color: [f32; 4]) {
1360 self.selection_color = color;
1361 }
1362
1363 pub fn set_cursor_color(&mut self, color: [f32; 4]) {
1365 self.cursor_color = color;
1366 }
1367
1368 pub fn set_text_color(&mut self, color: [f32; 4]) {
1371 self.text_color = color;
1372 }
1373
1374 pub fn text_color(&self) -> [f32; 4] {
1376 self.text_color
1377 }
1378
1379 pub fn block_visual_info(&self, block_id: usize) -> Option<BlockVisualInfo> {
1384 let block = self.flow_layout.blocks.get(&block_id)?;
1385 Some(BlockVisualInfo {
1386 block_id,
1387 y: block.y,
1388 height: block.height,
1389 })
1390 }
1391
1392 pub fn is_block_in_table(&self, block_id: usize) -> bool {
1394 self.flow_layout.tables.values().any(|table| {
1395 table
1396 .cell_layouts
1397 .iter()
1398 .any(|cell| cell.blocks.iter().any(|b| b.block_id == block_id))
1399 })
1400 }
1401
1402 pub fn scroll_to_position(&mut self, position: usize) -> f32 {
1405 let rect =
1406 crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position);
1407 let target_y = rect[1] + self.scroll_offset - self.viewport_height / (3.0 * self.zoom);
1408 self.scroll_offset = target_y.max(0.0);
1409 self.scroll_offset
1410 }
1411
1412 pub fn ensure_caret_visible(&mut self) -> Option<f32> {
1416 if self.cursors.is_empty() {
1417 return None;
1418 }
1419 let pos = self.cursors[0].position;
1420 let rect = crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, pos);
1421 let caret_screen_y = rect[1];
1422 let caret_screen_bottom = caret_screen_y + rect[3];
1423 let effective_vh = self.viewport_height / self.zoom;
1424 let margin = 10.0 / self.zoom;
1425 let old_offset = self.scroll_offset;
1426
1427 if caret_screen_y < 0.0 {
1428 self.scroll_offset += caret_screen_y - margin;
1429 self.scroll_offset = self.scroll_offset.max(0.0);
1430 } else if caret_screen_bottom > effective_vh {
1431 self.scroll_offset += caret_screen_bottom - effective_vh + margin;
1432 }
1433
1434 if (self.scroll_offset - old_offset).abs() > 0.001 {
1435 Some(self.scroll_offset)
1436 } else {
1437 None
1438 }
1439 }
1440}
1441
1442impl Default for DocumentFlow {
1443 fn default() -> Self {
1444 Self::new()
1445 }
1446}
1447
1448#[cfg(feature = "text-document")]
1449enum FlowItemKind {
1450 Block(BlockLayoutParams),
1451 Table(TableLayoutParams),
1452 Frame(FrameLayoutParams),
1453}
1454
1455fn rasterize_glyph_quad(
1460 service: &mut TextFontService,
1461 glyph: &ShapedGlyph,
1462 run: &ShapedRun,
1463 pen_x: f32,
1464 baseline: f32,
1465 text_color: [f32; 4],
1466 quads: &mut Vec<GlyphQuad>,
1467) {
1468 use crate::atlas::cache::GlyphCacheKey;
1469 use crate::atlas::rasterizer::rasterize_glyph;
1470
1471 if glyph.glyph_id == 0 {
1472 return;
1473 }
1474
1475 let entry = match service.font_registry.get(glyph.font_face_id) {
1476 Some(e) => e,
1477 None => return,
1478 };
1479
1480 let sf = service.scale_factor.max(f32::MIN_POSITIVE);
1481 let inv_sf = 1.0 / sf;
1482 let physical_size_px = run.size_px * sf;
1483 let cache_key = GlyphCacheKey::new(glyph.font_face_id, glyph.glyph_id, physical_size_px);
1484
1485 if service.glyph_cache.peek(&cache_key).is_none()
1486 && let Some(image) = rasterize_glyph(
1487 &mut service.scale_context,
1488 &entry.data,
1489 entry.face_index,
1490 entry.swash_cache_key,
1491 glyph.glyph_id,
1492 physical_size_px,
1493 )
1494 && image.width > 0
1495 && image.height > 0
1496 && let Some(alloc) = service.atlas.allocate(image.width, image.height)
1497 {
1498 let rect = alloc.rectangle;
1499 let atlas_x = rect.min.x as u32;
1500 let atlas_y = rect.min.y as u32;
1501 if image.is_color {
1502 service
1503 .atlas
1504 .blit_rgba(atlas_x, atlas_y, image.width, image.height, &image.data);
1505 } else {
1506 service
1507 .atlas
1508 .blit_mask(atlas_x, atlas_y, image.width, image.height, &image.data);
1509 }
1510 service.glyph_cache.insert(
1511 cache_key,
1512 crate::atlas::cache::CachedGlyph {
1513 alloc_id: alloc.id,
1514 atlas_x,
1515 atlas_y,
1516 width: image.width,
1517 height: image.height,
1518 placement_left: image.placement_left,
1519 placement_top: image.placement_top,
1520 is_color: image.is_color,
1521 last_used: 0,
1522 },
1523 );
1524 }
1525
1526 if let Some(cached) = service.glyph_cache.get(&cache_key) {
1527 let logical_w = cached.width as f32 * inv_sf;
1528 let logical_h = cached.height as f32 * inv_sf;
1529 let logical_left = cached.placement_left as f32 * inv_sf;
1530 let logical_top = cached.placement_top as f32 * inv_sf;
1531 let screen_x = pen_x + glyph.x_offset + logical_left;
1532 let screen_y = baseline - glyph.y_offset - logical_top;
1533 let color = if cached.is_color {
1534 [1.0, 1.0, 1.0, 1.0]
1535 } else {
1536 text_color
1537 };
1538 quads.push(GlyphQuad {
1539 screen: [screen_x, screen_y, logical_w, logical_h],
1540 atlas: [
1541 cached.atlas_x as f32,
1542 cached.atlas_y as f32,
1543 cached.width as f32,
1544 cached.height as f32,
1545 ],
1546 color,
1547 });
1548 }
1549}
1550
1551fn apply_zoom(frame: &mut RenderFrame, zoom: f32) {
1553 if (zoom - 1.0).abs() <= f32::EPSILON {
1554 return;
1555 }
1556 for q in &mut frame.glyphs {
1557 q.screen[0] *= zoom;
1558 q.screen[1] *= zoom;
1559 q.screen[2] *= zoom;
1560 q.screen[3] *= zoom;
1561 }
1562 for q in &mut frame.images {
1563 q.screen[0] *= zoom;
1564 q.screen[1] *= zoom;
1565 q.screen[2] *= zoom;
1566 q.screen[3] *= zoom;
1567 }
1568 apply_zoom_decorations(&mut frame.decorations, zoom);
1569}
1570
1571fn apply_zoom_decorations(decorations: &mut [DecorationRect], zoom: f32) {
1573 if (zoom - 1.0).abs() <= f32::EPSILON {
1574 return;
1575 }
1576 for d in decorations.iter_mut() {
1577 d.rect[0] *= zoom;
1578 d.rect[1] *= zoom;
1579 d.rect[2] *= zoom;
1580 d.rect[3] *= zoom;
1581 }
1582}
1583
1584fn merge_format(base: &TextFormat, attrs: InlineAttrs) -> TextFormat {
1587 let mut fmt = base.clone();
1588 if attrs.is_bold() {
1589 fmt.font_bold = Some(true);
1590 if let Some(w) = fmt.font_weight
1591 && w < 600
1592 {
1593 fmt.font_weight = Some(700);
1594 } else if fmt.font_weight.is_none() {
1595 fmt.font_weight = Some(700);
1596 }
1597 }
1598 if attrs.is_italic() {
1599 fmt.font_italic = Some(true);
1600 }
1601 fmt
1602}
1603
1604#[cfg(test)]
1605mod tests {
1606 use super::*;
1607 use crate::layout::block::{BlockLayoutParams, FragmentParams};
1608 use crate::layout::paragraph::Alignment;
1609 use crate::types::{UnderlineStyle, VerticalAlignment};
1610
1611 const NOTO_SANS: &[u8] = include_bytes!("../test-fonts/NotoSans-Variable.ttf");
1612
1613 fn service() -> TextFontService {
1614 let mut s = TextFontService::new();
1615 let face = s.register_font(NOTO_SANS);
1616 s.set_default_font(face, 16.0);
1617 s
1618 }
1619
1620 fn block(id: usize, text: &str) -> BlockLayoutParams {
1621 BlockLayoutParams {
1622 block_id: id,
1623 position: 0,
1624 text: text.to_string(),
1625 fragments: vec![FragmentParams {
1626 text: text.to_string(),
1627 offset: 0,
1628 length: text.len(),
1629 font_family: None,
1630 font_weight: None,
1631 font_bold: None,
1632 font_italic: None,
1633 font_point_size: None,
1634 underline_style: UnderlineStyle::None,
1635 overline: false,
1636 strikeout: false,
1637 is_link: false,
1638 letter_spacing: 0.0,
1639 word_spacing: 0.0,
1640 foreground_color: None,
1641 underline_color: None,
1642 background_color: None,
1643 anchor_href: None,
1644 tooltip: None,
1645 vertical_alignment: VerticalAlignment::Normal,
1646 image_name: None,
1647 image_width: 0.0,
1648 image_height: 0.0,
1649 }],
1650 alignment: Alignment::Left,
1651 top_margin: 0.0,
1652 bottom_margin: 0.0,
1653 left_margin: 0.0,
1654 right_margin: 0.0,
1655 text_indent: 0.0,
1656 list_marker: String::new(),
1657 list_indent: 0.0,
1658 tab_positions: vec![],
1659 line_height_multiplier: None,
1660 non_breakable_lines: false,
1661 checkbox: None,
1662 background_color: None,
1663 }
1664 }
1665
1666 #[test]
1667 fn relayout_block_returns_no_layout_when_never_laid_out() {
1668 let svc = service();
1669 let mut flow = DocumentFlow::new();
1670 flow.set_viewport(400.0, 200.0);
1671 let err = flow.relayout_block(&svc, &block(1, "Hello")).unwrap_err();
1672 assert_eq!(err, RelayoutError::NoLayout);
1673 }
1674
1675 #[test]
1676 fn relayout_block_returns_scale_dirty_after_scale_factor_change() {
1677 let mut svc = service();
1678 let mut flow = DocumentFlow::new();
1679 flow.set_viewport(400.0, 200.0);
1680 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
1681 assert!(flow.has_layout());
1682
1683 svc.set_scale_factor(2.0);
1685 assert!(flow.layout_dirty_for_scale(&svc));
1686
1687 let err = flow
1688 .relayout_block(&svc, &block(1, "Hello world"))
1689 .unwrap_err();
1690 assert_eq!(err, RelayoutError::ScaleDirty);
1691 }
1692
1693 #[test]
1694 fn relayout_block_succeeds_after_fresh_layout_post_scale_change() {
1695 let mut svc = service();
1696 let mut flow = DocumentFlow::new();
1697 flow.set_viewport(400.0, 200.0);
1698 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
1699
1700 svc.set_scale_factor(2.0);
1701 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
1704 assert!(!flow.layout_dirty_for_scale(&svc));
1705
1706 flow.relayout_block(&svc, &block(1, "Hello world"))
1708 .expect("relayout_block must succeed after a fresh post-scale layout");
1709 }
1710}