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 code_block_background: [f32; 4],
143 code_block_foreground: Option<[f32; 4]>,
148 echo_char: Option<char>,
155 cursors: Vec<CursorDisplay>,
156 zoom: f32,
157 rendered_zoom: f32,
158 layout_scale_generation: u64,
164 has_layout: bool,
166}
167
168impl DocumentFlow {
169 pub fn new() -> Self {
175 Self {
176 flow_layout: FlowLayout::new(),
177 render_frame: RenderFrame::new(),
178 scroll_offset: 0.0,
179 rendered_scroll_offset: f32::NAN,
180 viewport_width: 0.0,
181 viewport_height: 0.0,
182 content_width_mode: ContentWidthMode::Auto,
183 selection_color: [0.26, 0.52, 0.96, 0.3],
184 cursor_color: [0.0, 0.0, 0.0, 1.0],
185 text_color: [0.0, 0.0, 0.0, 1.0],
186 code_block_background: [0.95, 0.95, 0.95, 1.0],
187 code_block_foreground: None,
188 echo_char: None,
189 cursors: Vec::new(),
190 zoom: 1.0,
191 rendered_zoom: f32::NAN,
192 layout_scale_generation: 0,
193 has_layout: false,
194 }
195 }
196
197 pub fn set_viewport(&mut self, width: f32, height: f32) {
213 self.viewport_width = width;
214 self.viewport_height = height;
215 self.flow_layout.viewport_width = width;
216 self.flow_layout.viewport_height = height;
217 }
218
219 pub fn viewport_width(&self) -> f32 {
221 self.viewport_width
222 }
223
224 pub fn viewport_height(&self) -> f32 {
226 self.viewport_height
227 }
228
229 pub fn set_content_width(&mut self, width: f32) {
235 self.content_width_mode = ContentWidthMode::Fixed(width);
236 }
237
238 pub fn set_content_width_auto(&mut self) {
243 self.content_width_mode = ContentWidthMode::Auto;
244 }
245
246 pub fn layout_width(&self) -> f32 {
254 match self.content_width_mode {
255 ContentWidthMode::Auto => self.viewport_width / self.zoom,
256 ContentWidthMode::Fixed(w) => w,
257 }
258 }
259
260 pub fn content_width_mode(&self) -> ContentWidthMode {
262 self.content_width_mode
263 }
264
265 pub fn set_scroll_offset(&mut self, offset: f32) {
269 self.scroll_offset = offset;
270 }
271
272 pub fn scroll_offset(&self) -> f32 {
274 self.scroll_offset
275 }
276
277 pub fn content_height(&self) -> f32 {
279 self.flow_layout.content_height
280 }
281
282 pub fn max_content_width(&self) -> f32 {
286 self.flow_layout.cached_max_content_width
287 }
288
289 pub fn set_zoom(&mut self, zoom: f32) {
303 self.zoom = zoom.clamp(0.1, 10.0);
304 }
305
306 pub fn zoom(&self) -> f32 {
308 self.zoom
309 }
310
311 pub fn has_layout(&self) -> bool {
318 self.has_layout
319 }
320
321 pub fn layout_dirty_for_scale(&self, service: &TextFontService) -> bool {
331 self.has_layout && self.layout_scale_generation != service.scale_generation()
332 }
333
334 #[cfg(feature = "text-document")]
343 pub fn layout_full(&mut self, service: &TextFontService, flow: &text_document::FlowSnapshot) {
344 use crate::bridge::{BridgeOptions, convert_flow_with};
345
346 let opts = BridgeOptions {
347 code_block_background: self.code_block_background,
348 code_block_foreground: self.code_block_foreground,
349 echo_char: self.echo_char,
350 };
351 let converted = convert_flow_with(flow, &opts);
352
353 let mut all_items: Vec<(usize, FlowItemKind)> = Vec::new();
355 for (idx, params) in converted.blocks {
356 all_items.push((idx, FlowItemKind::Block(params)));
357 }
358 for (idx, params) in converted.tables {
359 all_items.push((idx, FlowItemKind::Table(params)));
360 }
361 for (idx, params) in converted.frames {
362 all_items.push((idx, FlowItemKind::Frame(params)));
363 }
364 all_items.sort_by_key(|(idx, _)| *idx);
365
366 let lw = self.layout_width();
367 self.flow_layout.clear();
368 self.flow_layout.viewport_width = self.viewport_width;
369 self.flow_layout.viewport_height = self.viewport_height;
370 self.flow_layout.scale_factor = service.scale_factor;
371
372 for (_idx, kind) in all_items {
373 match kind {
374 FlowItemKind::Block(params) => {
375 self.flow_layout
376 .add_block(&service.font_registry, ¶ms, lw);
377 }
378 FlowItemKind::Table(params) => {
379 self.flow_layout
380 .add_table(&service.font_registry, ¶ms, lw);
381 }
382 FlowItemKind::Frame(params) => {
383 self.flow_layout
384 .add_frame(&service.font_registry, ¶ms, lw);
385 }
386 }
387 }
388
389 self.flow_layout.refresh_base_blocks();
392
393 self.note_layout_done(service);
394 }
395
396 pub fn layout_blocks(
402 &mut self,
403 service: &TextFontService,
404 block_params: Vec<BlockLayoutParams>,
405 ) {
406 self.flow_layout.scale_factor = service.scale_factor;
407 self.flow_layout
408 .layout_blocks(&service.font_registry, block_params, self.layout_width());
409 self.note_layout_done(service);
410 }
411
412 pub fn add_frame(&mut self, service: &TextFontService, params: &FrameLayoutParams) {
415 self.flow_layout.scale_factor = service.scale_factor;
416 self.flow_layout
417 .add_frame(&service.font_registry, params, self.layout_width());
418 self.note_layout_done(service);
419 }
420
421 pub fn add_table(&mut self, service: &TextFontService, params: &TableLayoutParams) {
423 self.flow_layout.scale_factor = service.scale_factor;
424 self.flow_layout
425 .add_table(&service.font_registry, params, self.layout_width());
426 self.note_layout_done(service);
427 }
428
429 pub fn relayout_block(
460 &mut self,
461 service: &TextFontService,
462 params: &BlockLayoutParams,
463 ) -> Result<(), RelayoutError> {
464 if !self.has_layout {
465 return Err(RelayoutError::NoLayout);
466 }
467 if self.layout_scale_generation != service.scale_generation() {
468 return Err(RelayoutError::ScaleDirty);
469 }
470 self.flow_layout.scale_factor = service.scale_factor;
471 self.flow_layout
472 .relayout_block(&service.font_registry, params, self.layout_width());
473 self.note_layout_done(service);
474 Ok(())
475 }
476
477 pub fn apply_paint_spans_for(
482 &mut self,
483 spans_by_block: std::collections::HashMap<usize, Vec<crate::layout::block::PaintSpan>>,
484 ) {
485 self.flow_layout.apply_paint_spans_for(spans_by_block);
486 }
487
488 pub fn apply_block_paint_spans(
491 &mut self,
492 block_id: usize,
493 spans: &[crate::layout::block::PaintSpan],
494 ) -> bool {
495 self.flow_layout.apply_block_paint_spans(block_id, spans)
496 }
497
498 fn note_layout_done(&mut self, service: &TextFontService) {
499 self.has_layout = true;
500 self.layout_scale_generation = service.scale_generation();
501 }
502
503 pub fn render(&mut self, service: &mut TextFontService) -> &RenderFrame {
515 let effective_vw = self.viewport_width / self.zoom;
516 let effective_vh = self.viewport_height / self.zoom;
517 crate::render::frame::build_render_frame(
518 &self.flow_layout,
519 &service.font_registry,
520 &mut service.atlas,
521 &mut service.glyph_cache,
522 &mut service.scale_context,
523 self.scroll_offset,
524 effective_vw,
525 effective_vh,
526 &self.cursors,
527 self.cursor_color,
528 self.selection_color,
529 self.text_color,
530 &mut self.render_frame,
531 );
532 self.rendered_scroll_offset = self.scroll_offset;
533 self.rendered_zoom = self.zoom;
534 apply_zoom(&mut self.render_frame, self.zoom);
535 &self.render_frame
536 }
537
538 pub fn render_block_only(
551 &mut self,
552 service: &mut TextFontService,
553 block_id: usize,
554 ) -> &RenderFrame {
555 if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
556 || (self.zoom - self.rendered_zoom).abs() > 0.001
557 {
558 return self.render(service);
559 }
560
561 if !self.flow_layout.blocks.contains_key(&block_id) {
562 let in_table = self.flow_layout.tables.values().any(|table| {
563 table
564 .cell_layouts
565 .iter()
566 .any(|c| c.blocks.iter().any(|b| b.block_id == block_id))
567 });
568 if in_table {
569 return self.render(service);
570 }
571 let in_frame = self
572 .flow_layout
573 .frames
574 .values()
575 .any(|frame| crate::layout::flow::frame_contains_block(frame, block_id));
576 if in_frame {
577 return self.render(service);
578 }
579 }
580
581 if let Some(block) = self.flow_layout.blocks.get(&block_id) {
582 let old_height = self
583 .render_frame
584 .block_heights
585 .get(&block_id)
586 .copied()
587 .unwrap_or(block.height);
588 if (block.height - old_height).abs() > 0.001 {
589 return self.render(service);
590 }
591 }
592
593 let effective_vw = self.viewport_width / self.zoom;
594 let effective_vh = self.viewport_height / self.zoom;
595 let scale_factor = service.scale_factor;
596 let mut new_glyphs = Vec::new();
597 let mut new_images = Vec::new();
598 if let Some(block) = self.flow_layout.blocks.get(&block_id) {
599 let mut tmp = RenderFrame::new();
600 crate::render::frame::render_block_at_offset(
601 block,
602 0.0,
603 0.0,
604 &service.font_registry,
605 &mut service.atlas,
606 &mut service.glyph_cache,
607 &mut service.scale_context,
608 self.scroll_offset,
609 effective_vh,
610 self.text_color,
611 scale_factor,
612 &mut tmp,
613 );
614 new_glyphs = tmp.glyphs;
615 new_images = tmp.images;
616 }
617
618 let new_decos = if let Some(block) = self.flow_layout.blocks.get(&block_id) {
619 crate::render::decoration::generate_block_decorations(
620 block,
621 &service.font_registry,
622 self.scroll_offset,
623 effective_vh,
624 0.0,
625 0.0,
626 effective_vw,
627 self.text_color,
628 scale_factor,
629 )
630 } else {
631 Vec::new()
632 };
633
634 if let Some(entry) = self
635 .render_frame
636 .block_glyphs
637 .iter_mut()
638 .find(|(id, _)| *id == block_id)
639 {
640 entry.1 = new_glyphs;
641 }
642 if let Some(entry) = self
643 .render_frame
644 .block_images
645 .iter_mut()
646 .find(|(id, _)| *id == block_id)
647 {
648 entry.1 = new_images;
649 }
650 if let Some(entry) = self
651 .render_frame
652 .block_decorations
653 .iter_mut()
654 .find(|(id, _)| *id == block_id)
655 {
656 entry.1 = new_decos;
657 }
658
659 self.rebuild_flat_frame(service);
660 apply_zoom(&mut self.render_frame, self.zoom);
661 &self.render_frame
662 }
663
664 pub fn render_cursor_only(&mut self, service: &mut TextFontService) -> &RenderFrame {
672 if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
673 || (self.zoom - self.rendered_zoom).abs() > 0.001
674 {
675 return self.render(service);
676 }
677
678 self.render_frame.decorations.retain(|d| {
679 !matches!(
680 d.kind,
681 DecorationKind::Cursor | DecorationKind::Selection | DecorationKind::CellSelection
682 )
683 });
684
685 let effective_vw = self.viewport_width / self.zoom;
686 let effective_vh = self.viewport_height / self.zoom;
687 let mut cursor_decos = crate::render::cursor::generate_cursor_decorations(
688 &self.flow_layout,
689 &self.cursors,
690 self.scroll_offset,
691 self.cursor_color,
692 self.selection_color,
693 effective_vw,
694 effective_vh,
695 );
696 apply_zoom_decorations(&mut cursor_decos, self.zoom);
697 self.render_frame.decorations.extend(cursor_decos);
698
699 &self.render_frame
700 }
701
702 fn rebuild_flat_frame(&mut self, service: &mut TextFontService) {
703 self.render_frame.glyphs.clear();
704 self.render_frame.images.clear();
705 self.render_frame.decorations.clear();
706 for (_, glyphs) in &self.render_frame.block_glyphs {
707 self.render_frame.glyphs.extend_from_slice(glyphs);
708 }
709 for (_, images) in &self.render_frame.block_images {
710 self.render_frame.images.extend_from_slice(images);
711 }
712 for (_, decos) in &self.render_frame.block_decorations {
713 self.render_frame.decorations.extend_from_slice(decos);
714 }
715
716 for item in &self.flow_layout.flow_order {
717 match item {
718 FlowItem::Table { table_id, .. } => {
719 if let Some(table) = self.flow_layout.tables.get(table_id) {
720 let decos = crate::layout::table::generate_table_decorations(
721 table,
722 self.scroll_offset,
723 );
724 self.render_frame.decorations.extend(decos);
725 }
726 }
727 FlowItem::Frame { frame_id, .. } => {
728 if let Some(frame) = self.flow_layout.frames.get(frame_id) {
729 crate::render::frame::append_frame_border_decorations(
730 frame,
731 self.scroll_offset,
732 &mut self.render_frame.decorations,
733 );
734 }
735 }
736 FlowItem::Block { .. } => {}
737 }
738 }
739
740 let effective_vw = self.viewport_width / self.zoom;
741 let effective_vh = self.viewport_height / self.zoom;
742 let cursor_decos = crate::render::cursor::generate_cursor_decorations(
743 &self.flow_layout,
744 &self.cursors,
745 self.scroll_offset,
746 self.cursor_color,
747 self.selection_color,
748 effective_vw,
749 effective_vh,
750 );
751 self.render_frame.decorations.extend(cursor_decos);
752
753 self.render_frame.atlas_dirty = service.atlas.dirty;
754 self.render_frame.atlas_width = service.atlas.width;
755 self.render_frame.atlas_height = service.atlas.height;
756 if service.atlas.dirty {
757 let pixels = &service.atlas.pixels;
758 let needed = (service.atlas.width * service.atlas.height * 4) as usize;
759 self.render_frame.atlas_pixels.resize(needed, 0);
760 let copy_len = needed.min(pixels.len());
761 self.render_frame.atlas_pixels[..copy_len].copy_from_slice(&pixels[..copy_len]);
762 service.atlas.dirty = false;
763 }
764 }
765
766 pub fn layout_single_line(
776 &mut self,
777 service: &mut TextFontService,
778 text: &str,
779 format: &TextFormat,
780 max_width: Option<f32>,
781 ) -> SingleLineResult {
782 let empty = SingleLineResult {
783 width: 0.0,
784 height: 0.0,
785 baseline: 0.0,
786 underline_offset: 0.0,
787 underline_thickness: 0.0,
788 glyphs: Vec::new(),
789 glyph_keys: Vec::new(),
790 spans: Vec::new(),
791 };
792
793 if text.is_empty() {
794 return empty;
795 }
796
797 let font_point_size = format.font_size.map(|s| s as u32);
798 let resolved = match resolve_font(
799 &service.font_registry,
800 format.font_family.as_deref(),
801 format.font_weight,
802 format.font_bold,
803 format.font_italic,
804 font_point_size,
805 service.scale_factor,
806 ) {
807 Some(r) => r,
808 None => return empty,
809 };
810
811 let metrics = match font_metrics_px(&service.font_registry, &resolved) {
812 Some(m) => m,
813 None => return empty,
814 };
815 let line_height = metrics.ascent + metrics.descent + metrics.leading;
816 let baseline = metrics.ascent;
817
818 let runs: Vec<_> = bidi_runs(text)
819 .into_iter()
820 .filter_map(|br| {
821 let slice = text.get(br.byte_range.clone())?;
822 shape_text_with_fallback(
823 &service.font_registry,
824 &resolved,
825 slice,
826 br.byte_range.start,
827 br.direction,
828 )
829 })
830 .collect();
831
832 if runs.is_empty() {
833 return empty;
834 }
835
836 let total_advance: f32 = runs.iter().map(|r| r.advance_width).sum();
837
838 let (truncate_at_visual_index, final_width, ellipsis_run) = if let Some(max_w) = max_width
839 && total_advance > max_w
840 {
841 let ellipsis_run = shape_text(&service.font_registry, &resolved, "\u{2026}", 0);
842 let ellipsis_width = ellipsis_run
843 .as_ref()
844 .map(|r| r.advance_width)
845 .unwrap_or(0.0);
846 let budget = (max_w - ellipsis_width).max(0.0);
847
848 let mut used = 0.0f32;
849 let mut count = 0usize;
850 'outer: for run in &runs {
851 for g in &run.glyphs {
852 if used + g.x_advance > budget {
853 break 'outer;
854 }
855 used += g.x_advance;
856 count += 1;
857 }
858 }
859
860 (Some(count), used + ellipsis_width, ellipsis_run)
861 } else {
862 (None, total_advance, None)
863 };
864
865 let text_color = format.color.unwrap_or(self.text_color);
866 let glyph_capacity: usize = runs.iter().map(|r| r.glyphs.len()).sum();
867 let mut quads = Vec::with_capacity(glyph_capacity + 1);
868 let mut keys = Vec::with_capacity(glyph_capacity + 1);
869 let mut pen_x = 0.0f32;
870 let mut emitted = 0usize;
871
872 'emit: for run in &runs {
873 for glyph in &run.glyphs {
874 if let Some(limit) = truncate_at_visual_index
875 && emitted >= limit
876 {
877 break 'emit;
878 }
879 rasterize_glyph_quad(
880 service, glyph, run, pen_x, baseline, text_color, &mut quads, &mut keys,
881 );
882 pen_x += glyph.x_advance;
883 emitted += 1;
884 }
885 }
886
887 if let Some(ref e_run) = ellipsis_run {
888 for glyph in &e_run.glyphs {
889 rasterize_glyph_quad(
890 service, glyph, e_run, pen_x, baseline, text_color, &mut quads, &mut keys,
891 );
892 pen_x += glyph.x_advance;
893 }
894 }
895
896 SingleLineResult {
897 width: final_width,
898 height: line_height,
899 baseline,
900 underline_offset: metrics.underline_offset,
901 underline_thickness: metrics.stroke_size,
902 glyphs: quads,
903 glyph_keys: keys,
904 spans: Vec::new(),
905 }
906 }
907
908 pub fn layout_paragraph(
919 &mut self,
920 service: &mut TextFontService,
921 text: &str,
922 format: &TextFormat,
923 max_width: f32,
924 max_lines: Option<usize>,
925 ) -> ParagraphResult {
926 let empty = ParagraphResult {
927 width: 0.0,
928 height: 0.0,
929 baseline_first: 0.0,
930 line_count: 0,
931 line_height: 0.0,
932 underline_offset: 0.0,
933 underline_thickness: 0.0,
934 glyphs: Vec::new(),
935 glyph_keys: Vec::new(),
936 spans: Vec::new(),
937 };
938
939 if text.is_empty() || max_width <= 0.0 {
940 return empty;
941 }
942
943 let font_point_size = format.font_size.map(|s| s as u32);
944 let resolved = match resolve_font(
945 &service.font_registry,
946 format.font_family.as_deref(),
947 format.font_weight,
948 format.font_bold,
949 format.font_italic,
950 font_point_size,
951 service.scale_factor,
952 ) {
953 Some(r) => r,
954 None => return empty,
955 };
956
957 let metrics = match font_metrics_px(&service.font_registry, &resolved) {
958 Some(m) => m,
959 None => return empty,
960 };
961
962 let runs: Vec<_> = bidi_runs(text)
963 .into_iter()
964 .filter_map(|br| {
965 let slice = text.get(br.byte_range.clone())?;
966 shape_text_with_fallback(
967 &service.font_registry,
968 &resolved,
969 slice,
970 br.byte_range.start,
971 br.direction,
972 )
973 })
974 .collect();
975
976 if runs.is_empty() {
977 return empty;
978 }
979
980 let lines = break_into_lines(runs, text, max_width, Alignment::Left, 0.0, &metrics);
981
982 let line_count = match max_lines {
983 Some(n) => lines.len().min(n),
984 None => lines.len(),
985 };
986
987 let text_color = format.color.unwrap_or(self.text_color);
988 let mut quads: Vec<GlyphQuad> = Vec::new();
989 let mut keys: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
990 let mut y_top = 0.0f32;
991 let mut max_line_width = 0.0f32;
992 let baseline_first = metrics.ascent;
993
994 for line in lines.iter().take(line_count) {
995 if line.width > max_line_width {
996 max_line_width = line.width;
997 }
998 let baseline_y = y_top + metrics.ascent;
999 for run in &line.runs {
1000 let mut pen_x = run.x;
1001 let run_copy = run.shaped_run.clone();
1002 for glyph in &run_copy.glyphs {
1003 rasterize_glyph_quad(
1004 service, glyph, &run_copy, pen_x, baseline_y, text_color, &mut quads,
1005 &mut keys,
1006 );
1007 pen_x += glyph.x_advance;
1008 }
1009 }
1010 y_top += metrics.ascent + metrics.descent + metrics.leading;
1011 }
1012
1013 let line_height = metrics.ascent + metrics.descent + metrics.leading;
1014 ParagraphResult {
1015 width: max_line_width,
1016 height: y_top,
1017 baseline_first,
1018 line_count,
1019 line_height,
1020 underline_offset: metrics.underline_offset,
1021 underline_thickness: metrics.stroke_size,
1022 glyphs: quads,
1023 glyph_keys: keys,
1024 spans: Vec::new(),
1025 }
1026 }
1027
1028 pub fn layout_single_line_markup(
1034 &mut self,
1035 service: &mut TextFontService,
1036 markup: &InlineMarkup,
1037 format: &TextFormat,
1038 max_width: Option<f32>,
1039 ) -> SingleLineResult {
1040 if markup.spans.is_empty() {
1041 return SingleLineResult {
1042 width: 0.0,
1043 height: 0.0,
1044 baseline: 0.0,
1045 underline_offset: 0.0,
1046 underline_thickness: 0.0,
1047 glyphs: Vec::new(),
1048 glyph_keys: Vec::new(),
1049 spans: Vec::new(),
1050 };
1051 }
1052
1053 let per_span: Vec<(SingleLineResult, &crate::layout::inline_markup::InlineSpan)> = markup
1054 .spans
1055 .iter()
1056 .map(|sp| {
1057 let fmt = merge_format(format, sp.attrs);
1058 let r = if sp.text.is_empty() {
1059 SingleLineResult {
1060 width: 0.0,
1061 height: 0.0,
1062 baseline: 0.0,
1063 underline_offset: 0.0,
1064 underline_thickness: 0.0,
1065 glyphs: Vec::new(),
1066 glyph_keys: Vec::new(),
1067 spans: Vec::new(),
1068 }
1069 } else {
1070 self.layout_single_line(service, &sp.text, &fmt, None)
1071 };
1072 (r, sp)
1073 })
1074 .collect();
1075
1076 let total_width: f32 = per_span.iter().map(|(r, _)| r.width).sum();
1077 let line_height = per_span
1078 .iter()
1079 .map(|(r, _)| r.height)
1080 .fold(0.0f32, f32::max);
1081 let baseline = per_span
1082 .iter()
1083 .map(|(r, _)| r.baseline)
1084 .fold(0.0f32, f32::max);
1085 let (underline_offset, underline_thickness) = per_span
1089 .iter()
1090 .map(|(r, _)| (r.underline_offset, r.underline_thickness))
1091 .find(|(_, t)| *t > 0.0)
1092 .unwrap_or((0.0, 0.0));
1093
1094 let truncate = match max_width {
1095 Some(mw) if total_width > mw => Some(mw),
1096 _ => None,
1097 };
1098
1099 let mut glyphs: Vec<GlyphQuad> = Vec::new();
1100 let mut all_keys: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
1101 let mut spans_out: Vec<LaidOutSpan> = Vec::new();
1102 let mut pen_x: f32 = 0.0;
1103 let effective_width = truncate.unwrap_or(total_width);
1104
1105 for (r, sp) in &per_span {
1106 let remaining = (effective_width - pen_x).max(0.0);
1107 let span_visible_width = r.width.min(remaining);
1108 if span_visible_width <= 0.0 && r.width > 0.0 {
1109 spans_out.push(LaidOutSpan {
1110 kind: if let Some(url) = sp.link_url.clone() {
1111 LaidOutSpanKind::Link { url }
1112 } else {
1113 LaidOutSpanKind::Text
1114 },
1115 line_index: 0,
1116 rect: [pen_x, 0.0, 0.0, line_height],
1117 byte_range: sp.byte_range.clone(),
1118 });
1119 continue;
1120 }
1121
1122 for (gi, g) in r.glyphs.iter().enumerate() {
1123 let g_right = pen_x + g.screen[0] + g.screen[2];
1124 if g_right > effective_width + 0.5 {
1125 break;
1126 }
1127 let mut gq = g.clone();
1128 gq.screen[0] += pen_x;
1129 glyphs.push(gq);
1130 if let Some(k) = r.glyph_keys.get(gi) {
1131 all_keys.push(*k);
1132 }
1133 }
1134
1135 spans_out.push(LaidOutSpan {
1136 kind: if let Some(url) = sp.link_url.clone() {
1137 LaidOutSpanKind::Link { url }
1138 } else {
1139 LaidOutSpanKind::Text
1140 },
1141 line_index: 0,
1142 rect: [pen_x, 0.0, span_visible_width, line_height],
1143 byte_range: sp.byte_range.clone(),
1144 });
1145
1146 pen_x += r.width;
1147 if truncate.is_some() && pen_x >= effective_width {
1148 break;
1149 }
1150 }
1151
1152 SingleLineResult {
1153 width: effective_width,
1154 height: line_height,
1155 baseline,
1156 underline_offset,
1157 underline_thickness,
1158 glyphs,
1159 glyph_keys: all_keys,
1160 spans: spans_out,
1161 }
1162 }
1163
1164 pub fn layout_paragraph_markup(
1169 &mut self,
1170 service: &mut TextFontService,
1171 markup: &InlineMarkup,
1172 format: &TextFormat,
1173 max_width: f32,
1174 max_lines: Option<usize>,
1175 ) -> ParagraphResult {
1176 let empty = ParagraphResult {
1177 width: 0.0,
1178 height: 0.0,
1179 baseline_first: 0.0,
1180 line_count: 0,
1181 line_height: 0.0,
1182 underline_offset: 0.0,
1183 underline_thickness: 0.0,
1184 glyphs: Vec::new(),
1185 glyph_keys: Vec::new(),
1186 spans: Vec::new(),
1187 };
1188
1189 if markup.spans.is_empty() || max_width <= 0.0 {
1190 return empty;
1191 }
1192
1193 let mut flat = String::new();
1194 let mut span_flat_offsets: Vec<usize> = Vec::with_capacity(markup.spans.len());
1195 for sp in &markup.spans {
1196 span_flat_offsets.push(flat.len());
1197 flat.push_str(&sp.text);
1198 }
1199 if flat.is_empty() {
1200 return empty;
1201 }
1202
1203 let base_point_size = format.font_size.map(|s| s as u32);
1204 let base_resolved = match resolve_font(
1205 &service.font_registry,
1206 format.font_family.as_deref(),
1207 format.font_weight,
1208 format.font_bold,
1209 format.font_italic,
1210 base_point_size,
1211 service.scale_factor,
1212 ) {
1213 Some(r) => r,
1214 None => return empty,
1215 };
1216 let metrics = match font_metrics_px(&service.font_registry, &base_resolved) {
1217 Some(m) => m,
1218 None => return empty,
1219 };
1220
1221 let mut all_runs: Vec<ShapedRun> = Vec::new();
1222 for (span_idx, sp) in markup.spans.iter().enumerate() {
1223 if sp.text.is_empty() {
1224 continue;
1225 }
1226 let fmt = merge_format(format, sp.attrs);
1227 let span_point_size = fmt.font_size.map(|s| s as u32);
1228 let Some(resolved) = resolve_font(
1229 &service.font_registry,
1230 fmt.font_family.as_deref(),
1231 fmt.font_weight,
1232 fmt.font_bold,
1233 fmt.font_italic,
1234 span_point_size,
1235 service.scale_factor,
1236 ) else {
1237 continue;
1238 };
1239
1240 let flat_start = span_flat_offsets[span_idx];
1241 for br in bidi_runs(&sp.text) {
1242 let slice = match sp.text.get(br.byte_range.clone()) {
1243 Some(s) => s,
1244 None => continue,
1245 };
1246 let Some(mut run) = shape_text_with_fallback(
1247 &service.font_registry,
1248 &resolved,
1249 slice,
1250 flat_start + br.byte_range.start,
1251 br.direction,
1252 ) else {
1253 continue;
1254 };
1255 if let Some(url) = sp.link_url.as_ref() {
1256 run.is_link = true;
1257 run.anchor_href = Some(url.clone());
1258 }
1259 all_runs.push(run);
1260 }
1261 }
1262
1263 if all_runs.is_empty() {
1264 return empty;
1265 }
1266
1267 let lines = break_into_lines(all_runs, &flat, max_width, Alignment::Left, 0.0, &metrics);
1268
1269 let line_count = match max_lines {
1270 Some(n) => lines.len().min(n),
1271 None => lines.len(),
1272 };
1273
1274 let text_color = format.color.unwrap_or(self.text_color);
1275 let mut glyphs_out: Vec<GlyphQuad> = Vec::new();
1276 let mut keys_out: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
1277 let mut spans_out: Vec<LaidOutSpan> = Vec::new();
1278 let line_height = metrics.ascent + metrics.descent + metrics.leading;
1279 let mut y_top: f32 = 0.0;
1280 let mut max_line_width: f32 = 0.0;
1281 let baseline_first = metrics.ascent;
1282
1283 for (line_idx, line) in lines.iter().take(line_count).enumerate() {
1284 if line.width > max_line_width {
1285 max_line_width = line.width;
1286 }
1287 let baseline_y = y_top + metrics.ascent;
1288
1289 for pr in &line.runs {
1290 let run_copy = pr.shaped_run.clone();
1291 let mut pen_x = pr.x;
1292 for glyph in &run_copy.glyphs {
1293 rasterize_glyph_quad(
1294 service,
1295 glyph,
1296 &run_copy,
1297 pen_x,
1298 baseline_y,
1299 text_color,
1300 &mut glyphs_out,
1301 &mut keys_out,
1302 );
1303 pen_x += glyph.x_advance;
1304 }
1305
1306 if pr.decorations.is_link
1307 && let Some(url) = pr.decorations.anchor_href.clone()
1308 {
1309 let width = pr.shaped_run.advance_width;
1310 spans_out.push(LaidOutSpan {
1311 kind: LaidOutSpanKind::Link { url },
1312 line_index: line_idx,
1313 rect: [pr.x, y_top, width, line_height],
1314 byte_range: pr.shaped_run.text_range.clone(),
1315 });
1316 }
1317 }
1318
1319 y_top += line_height;
1320 }
1321
1322 ParagraphResult {
1323 width: max_line_width,
1324 height: y_top,
1325 baseline_first,
1326 line_count,
1327 line_height,
1328 underline_offset: metrics.underline_offset,
1329 underline_thickness: metrics.stroke_size,
1330 glyphs: glyphs_out,
1331 glyph_keys: keys_out,
1332 spans: spans_out,
1333 }
1334 }
1335
1336 pub fn hit_test(&self, x: f32, y: f32) -> Option<HitTestResult> {
1343 crate::render::hit_test::hit_test(
1344 &self.flow_layout,
1345 self.scroll_offset,
1346 x / self.zoom,
1347 y / self.zoom,
1348 )
1349 }
1350
1351 pub fn character_geometry(
1360 &self,
1361 block_id: usize,
1362 char_start: usize,
1363 char_end: usize,
1364 ) -> Vec<CharacterGeometry> {
1365 if char_start >= char_end {
1366 return Vec::new();
1367 }
1368 let block = match self.flow_layout.blocks.get(&block_id) {
1369 Some(b) => b,
1370 None => return Vec::new(),
1371 };
1372
1373 let mut absolute: Vec<(usize, f32)> = Vec::with_capacity(char_end - char_start);
1374 for line in &block.lines {
1375 if line.char_range.end <= char_start || line.char_range.start >= char_end {
1376 continue;
1377 }
1378 let local_start = char_start.max(line.char_range.start);
1379 let local_end = char_end.min(line.char_range.end);
1380 for c in local_start..local_end {
1381 let x = line.x_for_offset(c);
1382 absolute.push((c, x));
1383 }
1384 if local_end == char_end {
1385 let x_end = line.x_for_offset(local_end);
1386 absolute.push((local_end, x_end));
1387 }
1388 }
1389
1390 if absolute.is_empty() {
1391 return Vec::new();
1392 }
1393
1394 absolute.sort_by_key(|(c, _)| *c);
1395
1396 let base_x = absolute.first().map(|(_, x)| *x).unwrap_or(0.0);
1397 let mut out: Vec<CharacterGeometry> = Vec::with_capacity(absolute.len());
1398 for window in absolute.windows(2) {
1399 let (c, x) = window[0];
1400 let (_, x_next) = window[1];
1401 if c >= char_end {
1402 break;
1403 }
1404 out.push(CharacterGeometry {
1405 position: x - base_x,
1406 width: (x_next - x).max(0.0),
1407 });
1408 }
1409 out
1410 }
1411
1412 pub fn caret_rect(&self, position: usize, affinity: crate::types::CursorAffinity) -> [f32; 4] {
1422 let mut rect = crate::render::hit_test::caret_rect(
1423 &self.flow_layout,
1424 self.scroll_offset,
1425 position,
1426 affinity,
1427 );
1428 rect[0] *= self.zoom;
1429 rect[1] *= self.zoom;
1430 rect[2] *= self.zoom;
1431 rect[3] *= self.zoom;
1432 rect
1433 }
1434
1435 pub fn set_cursor(&mut self, cursor: &CursorDisplay) {
1439 self.cursors = vec![CursorDisplay {
1440 position: cursor.position,
1441 anchor: cursor.anchor,
1442 affinity: cursor.affinity,
1443 visible: cursor.visible,
1444 selected_cells: cursor.selected_cells.clone(),
1445 }];
1446 }
1447
1448 pub fn set_cursors(&mut self, cursors: &[CursorDisplay]) {
1452 self.cursors = cursors
1453 .iter()
1454 .map(|c| CursorDisplay {
1455 position: c.position,
1456 anchor: c.anchor,
1457 affinity: c.affinity,
1458 visible: c.visible,
1459 selected_cells: c.selected_cells.clone(),
1460 })
1461 .collect();
1462 }
1463
1464 pub fn set_selection_color(&mut self, color: [f32; 4]) {
1467 self.selection_color = color;
1468 }
1469
1470 pub fn set_cursor_color(&mut self, color: [f32; 4]) {
1472 self.cursor_color = color;
1473 }
1474
1475 pub fn set_text_color(&mut self, color: [f32; 4]) {
1478 self.text_color = color;
1479 }
1480
1481 pub fn text_color(&self) -> [f32; 4] {
1483 self.text_color
1484 }
1485
1486 pub fn set_code_block_background(&mut self, color: [f32; 4]) {
1493 self.code_block_background = color;
1494 }
1495
1496 pub fn code_block_background(&self) -> [f32; 4] {
1498 self.code_block_background
1499 }
1500
1501 pub fn set_code_block_foreground(&mut self, color: Option<[f32; 4]>) {
1506 self.code_block_foreground = color;
1507 }
1508
1509 pub fn code_block_foreground(&self) -> Option<[f32; 4]> {
1511 self.code_block_foreground
1512 }
1513
1514 pub fn set_echo_char(&mut self, echo: Option<char>) {
1529 self.echo_char = echo;
1530 }
1531
1532 pub fn echo_char(&self) -> Option<char> {
1534 self.echo_char
1535 }
1536
1537 pub fn block_visual_info(&self, block_id: usize) -> Option<BlockVisualInfo> {
1542 let block = self.flow_layout.blocks.get(&block_id)?;
1543 Some(BlockVisualInfo {
1544 block_id,
1545 y: block.y,
1546 height: block.height,
1547 })
1548 }
1549
1550 pub fn is_block_in_table(&self, block_id: usize) -> bool {
1552 self.flow_layout.tables.values().any(|table| {
1553 table
1554 .cell_layouts
1555 .iter()
1556 .any(|cell| cell.blocks.iter().any(|b| b.block_id == block_id))
1557 })
1558 }
1559
1560 pub fn scroll_to_position(&mut self, position: usize) -> f32 {
1565 let rect = crate::render::hit_test::caret_rect(
1566 &self.flow_layout,
1567 self.scroll_offset,
1568 position,
1569 crate::types::CursorAffinity::Downstream,
1570 );
1571 let target_y = rect[1] + self.scroll_offset - self.viewport_height / (3.0 * self.zoom);
1572 self.scroll_offset = target_y.max(0.0);
1573 self.scroll_offset
1574 }
1575
1576 pub fn ensure_caret_visible(&mut self) -> Option<f32> {
1580 if self.cursors.is_empty() {
1581 return None;
1582 }
1583 let pos = self.cursors[0].position;
1584 let affinity = self.cursors[0].affinity;
1585 let rect = crate::render::hit_test::caret_rect(
1586 &self.flow_layout,
1587 self.scroll_offset,
1588 pos,
1589 affinity,
1590 );
1591 let caret_screen_y = rect[1];
1592 let caret_screen_bottom = caret_screen_y + rect[3];
1593 let effective_vh = self.viewport_height / self.zoom;
1594 let margin = 10.0 / self.zoom;
1595 let old_offset = self.scroll_offset;
1596
1597 if caret_screen_y < 0.0 {
1598 self.scroll_offset += caret_screen_y - margin;
1599 self.scroll_offset = self.scroll_offset.max(0.0);
1600 } else if caret_screen_bottom > effective_vh {
1601 self.scroll_offset += caret_screen_bottom - effective_vh + margin;
1602 }
1603
1604 if (self.scroll_offset - old_offset).abs() > 0.001 {
1605 Some(self.scroll_offset)
1606 } else {
1607 None
1608 }
1609 }
1610}
1611
1612impl Default for DocumentFlow {
1613 fn default() -> Self {
1614 Self::new()
1615 }
1616}
1617
1618#[cfg(feature = "text-document")]
1619enum FlowItemKind {
1620 Block(BlockLayoutParams),
1621 Table(TableLayoutParams),
1622 Frame(FrameLayoutParams),
1623}
1624
1625#[allow(clippy::too_many_arguments)]
1630fn rasterize_glyph_quad(
1631 service: &mut TextFontService,
1632 glyph: &ShapedGlyph,
1633 run: &ShapedRun,
1634 pen_x: f32,
1635 baseline: f32,
1636 text_color: [f32; 4],
1637 quads: &mut Vec<GlyphQuad>,
1638 glyph_keys: &mut Vec<crate::atlas::cache::GlyphCacheKey>,
1639) {
1640 use crate::atlas::cache::GlyphCacheKey;
1641 use crate::atlas::rasterizer::rasterize_glyph;
1642
1643 if glyph.glyph_id == 0 {
1644 return;
1645 }
1646
1647 let entry = match service.font_registry.get(glyph.font_face_id) {
1648 Some(e) => e,
1649 None => return,
1650 };
1651
1652 let sf = service.scale_factor.max(f32::MIN_POSITIVE);
1653 let inv_sf = 1.0 / sf;
1654 let physical_size_px = run.size_px * sf;
1655 let cache_key = GlyphCacheKey::with_weight(
1656 glyph.font_face_id,
1657 glyph.glyph_id,
1658 physical_size_px,
1659 run.weight as u32,
1660 );
1661
1662 if service.glyph_cache.peek(&cache_key).is_none()
1663 && let Some(image) = rasterize_glyph(
1664 &mut service.scale_context,
1665 entry.bytes(),
1666 entry.face_index,
1667 entry.swash_cache_key,
1668 glyph.glyph_id,
1669 physical_size_px,
1670 run.weight as u32,
1671 )
1672 && image.width > 0
1673 && image.height > 0
1674 && let Some(alloc) = service.atlas.allocate(image.width, image.height)
1675 {
1676 let rect = alloc.rectangle;
1677 let atlas_x = rect.min.x as u32;
1678 let atlas_y = rect.min.y as u32;
1679 if image.is_color {
1680 service
1681 .atlas
1682 .blit_rgba(atlas_x, atlas_y, image.width, image.height, &image.data);
1683 } else {
1684 service
1685 .atlas
1686 .blit_mask(atlas_x, atlas_y, image.width, image.height, &image.data);
1687 }
1688 service.glyph_cache.insert(
1689 cache_key,
1690 crate::atlas::cache::CachedGlyph {
1691 alloc_id: alloc.id,
1692 atlas_x,
1693 atlas_y,
1694 width: image.width,
1695 height: image.height,
1696 placement_left: image.placement_left,
1697 placement_top: image.placement_top,
1698 is_color: image.is_color,
1699 last_used: 0,
1700 },
1701 );
1702 }
1703
1704 if let Some(cached) = service.glyph_cache.get(&cache_key) {
1705 let logical_w = cached.width as f32 * inv_sf;
1706 let logical_h = cached.height as f32 * inv_sf;
1707 let logical_left = cached.placement_left as f32 * inv_sf;
1708 let logical_top = cached.placement_top as f32 * inv_sf;
1709 let screen_x = pen_x + glyph.x_offset + logical_left;
1710 let screen_y = baseline - glyph.y_offset - logical_top;
1711 let color = if cached.is_color {
1712 [1.0, 1.0, 1.0, 1.0]
1713 } else {
1714 text_color
1715 };
1716 quads.push(GlyphQuad {
1717 screen: [screen_x, screen_y, logical_w, logical_h],
1718 atlas: [
1719 cached.atlas_x as f32,
1720 cached.atlas_y as f32,
1721 cached.width as f32,
1722 cached.height as f32,
1723 ],
1724 color,
1725 is_color: cached.is_color,
1726 });
1727 glyph_keys.push(cache_key);
1728 }
1729}
1730
1731fn apply_zoom(frame: &mut RenderFrame, zoom: f32) {
1733 if (zoom - 1.0).abs() <= f32::EPSILON {
1734 return;
1735 }
1736 for q in &mut frame.glyphs {
1737 q.screen[0] *= zoom;
1738 q.screen[1] *= zoom;
1739 q.screen[2] *= zoom;
1740 q.screen[3] *= zoom;
1741 }
1742 for q in &mut frame.images {
1743 q.screen[0] *= zoom;
1744 q.screen[1] *= zoom;
1745 q.screen[2] *= zoom;
1746 q.screen[3] *= zoom;
1747 }
1748 apply_zoom_decorations(&mut frame.decorations, zoom);
1749}
1750
1751fn apply_zoom_decorations(decorations: &mut [DecorationRect], zoom: f32) {
1753 if (zoom - 1.0).abs() <= f32::EPSILON {
1754 return;
1755 }
1756 for d in decorations.iter_mut() {
1757 d.rect[0] *= zoom;
1758 d.rect[1] *= zoom;
1759 d.rect[2] *= zoom;
1760 d.rect[3] *= zoom;
1761 }
1762}
1763
1764fn merge_format(base: &TextFormat, attrs: InlineAttrs) -> TextFormat {
1767 let mut fmt = base.clone();
1768 if attrs.is_bold() {
1769 fmt.font_bold = Some(true);
1770 if let Some(w) = fmt.font_weight
1771 && w < 600
1772 {
1773 fmt.font_weight = Some(700);
1774 } else if fmt.font_weight.is_none() {
1775 fmt.font_weight = Some(700);
1776 }
1777 }
1778 if attrs.is_italic() {
1779 fmt.font_italic = Some(true);
1780 }
1781 fmt
1782}
1783
1784#[cfg(test)]
1785mod tests {
1786 use super::*;
1787 use crate::layout::block::{BlockLayoutParams, FragmentParams};
1788 use crate::layout::paragraph::Alignment;
1789 use crate::types::{UnderlineStyle, VerticalAlignment};
1790
1791 const NOTO_SANS: &[u8] = include_bytes!("../test-fonts/NotoSans-Variable.ttf");
1792
1793 fn service() -> TextFontService {
1794 let mut s = TextFontService::new();
1795 let face = s.register_font(NOTO_SANS);
1796 s.set_default_font(face, 16.0);
1797 s
1798 }
1799
1800 fn block(id: usize, text: &str) -> BlockLayoutParams {
1801 BlockLayoutParams {
1802 block_id: id,
1803 position: 0,
1804 text: text.to_string(),
1805 fragments: vec![FragmentParams {
1806 text: text.to_string(),
1807 offset: 0,
1808 length: text.len(),
1809 font_family: None,
1810 font_weight: None,
1811 font_bold: None,
1812 font_italic: None,
1813 font_point_size: None,
1814 underline_style: UnderlineStyle::None,
1815 overline: false,
1816 strikeout: false,
1817 is_link: false,
1818 letter_spacing: 0.0,
1819 word_spacing: 0.0,
1820 foreground_color: None,
1821 underline_color: None,
1822 background_color: None,
1823 anchor_href: None,
1824 tooltip: None,
1825 vertical_alignment: VerticalAlignment::Normal,
1826 image_name: None,
1827 image_width: 0.0,
1828 image_height: 0.0,
1829 }],
1830 alignment: Alignment::Left,
1831 top_margin: 0.0,
1832 bottom_margin: 0.0,
1833 left_margin: 0.0,
1834 right_margin: 0.0,
1835 text_indent: 0.0,
1836 list_marker: String::new(),
1837 list_indent: 0.0,
1838 tab_positions: vec![],
1839 line_height_multiplier: None,
1840 non_breakable_lines: false,
1841 checkbox: None,
1842 background_color: None,
1843 }
1844 }
1845
1846 #[test]
1847 fn relayout_block_returns_no_layout_when_never_laid_out() {
1848 let svc = service();
1849 let mut flow = DocumentFlow::new();
1850 flow.set_viewport(400.0, 200.0);
1851 let err = flow.relayout_block(&svc, &block(1, "Hello")).unwrap_err();
1852 assert_eq!(err, RelayoutError::NoLayout);
1853 }
1854
1855 #[test]
1856 fn relayout_block_returns_scale_dirty_after_scale_factor_change() {
1857 let mut svc = service();
1858 let mut flow = DocumentFlow::new();
1859 flow.set_viewport(400.0, 200.0);
1860 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
1861 assert!(flow.has_layout());
1862
1863 svc.set_scale_factor(2.0);
1865 assert!(flow.layout_dirty_for_scale(&svc));
1866
1867 let err = flow
1868 .relayout_block(&svc, &block(1, "Hello world"))
1869 .unwrap_err();
1870 assert_eq!(err, RelayoutError::ScaleDirty);
1871 }
1872
1873 #[test]
1874 fn relayout_block_succeeds_after_fresh_layout_post_scale_change() {
1875 let mut svc = service();
1876 let mut flow = DocumentFlow::new();
1877 flow.set_viewport(400.0, 200.0);
1878 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
1879
1880 svc.set_scale_factor(2.0);
1881 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
1884 assert!(!flow.layout_dirty_for_scale(&svc));
1885
1886 flow.relayout_block(&svc, &block(1, "Hello world"))
1888 .expect("relayout_block must succeed after a fresh post-scale layout");
1889 }
1890}