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, Hyphenator, break_into_lines};
52use crate::layout::table::TableLayoutParams;
53use crate::shaping::run::{ShapedGlyph, ShapedRun};
54use crate::shaping::shaper::{
55 bidi_runs, font_metrics_px, shape_text, shape_text_with_fallback, to_harfrust_features,
56};
57use crate::types::{
58 BlockVisualInfo, CharacterGeometry, CursorDisplay, DecorationKind, DecorationRect, GlyphQuad,
59 HitTestResult, LaidOutSpan, LaidOutSpanKind, ParagraphResult, RenderFrame, SingleLineResult,
60 TextFormat,
61};
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum RelayoutError {
75 NoLayout,
80 ScaleDirty,
88}
89
90impl std::fmt::Display for RelayoutError {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 match self {
93 RelayoutError::NoLayout => {
94 f.write_str("relayout_block called before any layout_* method")
95 }
96 RelayoutError::ScaleDirty => f.write_str(
97 "relayout_block called after a scale-factor change without a fresh layout_*",
98 ),
99 }
100 }
101}
102
103impl std::error::Error for RelayoutError {}
104
105#[derive(Debug, Clone, Copy, Default)]
110pub enum ContentWidthMode {
111 #[default]
115 Auto,
116 Fixed(f32),
120}
121
122pub struct DocumentFlow {
129 flow_layout: FlowLayout,
130 render_frame: RenderFrame,
131 scroll_offset: f32,
132 rendered_scroll_offset: f32,
133 viewport_width: f32,
134 viewport_height: f32,
135 content_width_mode: ContentWidthMode,
136 selection_color: [f32; 4],
137 cursor_color: [f32; 4],
138 text_color: [f32; 4],
139 code_block_background: [f32; 4],
145 code_block_foreground: Option<[f32; 4]>,
150 echo_char: Option<char>,
157 hyphenate_justified: bool,
163 cursors: Vec<CursorDisplay>,
164 zoom: f32,
165 rendered_zoom: f32,
166 raster_scale: f32,
175 rendered_raster_scale: f32,
176 layout_scale_generation: u64,
182 has_layout: bool,
184}
185
186impl DocumentFlow {
187 pub fn new() -> Self {
193 Self {
194 flow_layout: FlowLayout::new(),
195 render_frame: RenderFrame::new(),
196 scroll_offset: 0.0,
197 rendered_scroll_offset: f32::NAN,
198 viewport_width: 0.0,
199 viewport_height: 0.0,
200 content_width_mode: ContentWidthMode::Auto,
201 selection_color: [0.26, 0.52, 0.96, 0.3],
202 cursor_color: [0.0, 0.0, 0.0, 1.0],
203 text_color: [0.0, 0.0, 0.0, 1.0],
204 code_block_background: [0.95, 0.95, 0.95, 1.0],
205 code_block_foreground: None,
206 echo_char: None,
207 hyphenate_justified: false,
208 cursors: Vec::new(),
209 zoom: 1.0,
210 rendered_zoom: f32::NAN,
211 raster_scale: 1.0,
212 rendered_raster_scale: f32::NAN,
213 layout_scale_generation: 0,
214 has_layout: false,
215 }
216 }
217
218 pub fn set_viewport(&mut self, width: f32, height: f32) {
234 self.viewport_width = width;
235 self.viewport_height = height;
236 self.flow_layout.viewport_width = width;
237 self.flow_layout.viewport_height = height;
238 }
239
240 pub fn viewport_width(&self) -> f32 {
242 self.viewport_width
243 }
244
245 pub fn viewport_height(&self) -> f32 {
247 self.viewport_height
248 }
249
250 pub fn set_content_width(&mut self, width: f32) {
256 self.content_width_mode = ContentWidthMode::Fixed(width);
257 }
258
259 pub fn set_content_width_auto(&mut self) {
264 self.content_width_mode = ContentWidthMode::Auto;
265 }
266
267 pub fn layout_width(&self) -> f32 {
275 match self.content_width_mode {
276 ContentWidthMode::Auto => self.viewport_width / self.zoom,
277 ContentWidthMode::Fixed(w) => w,
278 }
279 }
280
281 pub fn content_width_mode(&self) -> ContentWidthMode {
283 self.content_width_mode
284 }
285
286 pub fn set_scroll_offset(&mut self, offset: f32) {
290 self.scroll_offset = offset;
291 }
292
293 pub fn scroll_offset(&self) -> f32 {
295 self.scroll_offset
296 }
297
298 pub fn content_height(&self) -> f32 {
300 self.flow_layout.content_height
301 }
302
303 pub fn max_content_width(&self) -> f32 {
307 self.flow_layout.cached_max_content_width
308 }
309
310 pub fn set_zoom(&mut self, zoom: f32) {
324 self.zoom = zoom.clamp(0.1, 10.0);
325 }
326
327 pub fn zoom(&self) -> f32 {
329 self.zoom
330 }
331
332 pub fn set_raster_scale(&mut self, raster_scale: f32) {
346 self.raster_scale = raster_scale.clamp(0.1, 16.0);
347 }
348
349 pub fn raster_scale(&self) -> f32 {
351 self.raster_scale
352 }
353
354 pub fn has_layout(&self) -> bool {
361 self.has_layout
362 }
363
364 pub fn layout_dirty_for_scale(&self, service: &TextFontService) -> bool {
374 self.has_layout && self.layout_scale_generation != service.scale_generation()
375 }
376
377 #[cfg(feature = "text-document")]
386 pub fn layout_full(&mut self, service: &TextFontService, flow: &text_document::FlowSnapshot) {
387 use crate::bridge::{BridgeOptions, convert_flow_with};
388
389 let opts = BridgeOptions {
390 code_block_background: self.code_block_background,
391 code_block_foreground: self.code_block_foreground,
392 echo_char: self.echo_char,
393 hyphenate_justified: self.hyphenate_justified,
394 };
395 let converted = convert_flow_with(flow, &opts);
396
397 let mut all_items: Vec<(usize, FlowItemKind)> = Vec::new();
399 for (idx, params) in converted.blocks {
400 all_items.push((idx, FlowItemKind::Block(params)));
401 }
402 for (idx, params) in converted.tables {
403 all_items.push((idx, FlowItemKind::Table(params)));
404 }
405 for (idx, params) in converted.frames {
406 all_items.push((idx, FlowItemKind::Frame(params)));
407 }
408 all_items.sort_by_key(|(idx, _)| *idx);
409
410 let lw = self.layout_width();
411 self.flow_layout.clear();
412 self.flow_layout.viewport_width = self.viewport_width;
413 self.flow_layout.viewport_height = self.viewport_height;
414 self.flow_layout.scale_factor = service.scale_factor;
415
416 for (_idx, kind) in all_items {
417 match kind {
418 FlowItemKind::Block(params) => {
419 self.flow_layout
420 .add_block(&service.font_registry, ¶ms, lw);
421 }
422 FlowItemKind::Table(params) => {
423 self.flow_layout
424 .add_table(&service.font_registry, ¶ms, lw);
425 }
426 FlowItemKind::Frame(params) => {
427 self.flow_layout
428 .add_frame(&service.font_registry, ¶ms, lw);
429 }
430 }
431 }
432
433 self.flow_layout.refresh_base_blocks();
436
437 self.note_layout_done(service);
438 }
439
440 pub fn layout_blocks(
446 &mut self,
447 service: &TextFontService,
448 block_params: Vec<BlockLayoutParams>,
449 ) {
450 self.flow_layout.scale_factor = service.scale_factor;
451 self.flow_layout
452 .layout_blocks(&service.font_registry, block_params, self.layout_width());
453 self.note_layout_done(service);
454 }
455
456 pub fn add_frame(&mut self, service: &TextFontService, params: &FrameLayoutParams) {
459 self.flow_layout.scale_factor = service.scale_factor;
460 self.flow_layout
461 .add_frame(&service.font_registry, params, self.layout_width());
462 self.note_layout_done(service);
463 }
464
465 pub fn add_table(&mut self, service: &TextFontService, params: &TableLayoutParams) {
467 self.flow_layout.scale_factor = service.scale_factor;
468 self.flow_layout
469 .add_table(&service.font_registry, params, self.layout_width());
470 self.note_layout_done(service);
471 }
472
473 pub fn relayout_block(
504 &mut self,
505 service: &TextFontService,
506 params: &BlockLayoutParams,
507 ) -> Result<(), RelayoutError> {
508 if !self.has_layout {
509 return Err(RelayoutError::NoLayout);
510 }
511 if self.layout_scale_generation != service.scale_generation() {
512 return Err(RelayoutError::ScaleDirty);
513 }
514 self.flow_layout.scale_factor = service.scale_factor;
515 self.flow_layout
516 .relayout_block(&service.font_registry, params, self.layout_width());
517 self.note_layout_done(service);
518 Ok(())
519 }
520
521 pub fn apply_paint_spans_for(
526 &mut self,
527 spans_by_block: std::collections::HashMap<usize, Vec<crate::layout::block::PaintSpan>>,
528 ) {
529 self.flow_layout.apply_paint_spans_for(spans_by_block);
530 }
531
532 pub fn apply_block_paint_spans(
535 &mut self,
536 block_id: usize,
537 spans: &[crate::layout::block::PaintSpan],
538 ) -> bool {
539 self.flow_layout.apply_block_paint_spans(block_id, spans)
540 }
541
542 fn note_layout_done(&mut self, service: &TextFontService) {
543 self.has_layout = true;
544 self.layout_scale_generation = service.scale_generation();
545 }
546
547 pub fn render(&mut self, service: &mut TextFontService) -> &RenderFrame {
559 let effective_vw = self.viewport_width / self.zoom;
560 let effective_vh = self.viewport_height / self.zoom;
561 crate::render::frame::build_render_frame(
562 &self.flow_layout,
563 &service.font_registry,
564 &mut service.atlas,
565 &mut service.glyph_cache,
566 &mut service.scale_context,
567 self.scroll_offset,
568 effective_vw,
569 effective_vh,
570 &self.cursors,
571 self.cursor_color,
572 self.selection_color,
573 self.text_color,
574 self.raster_scale,
575 &mut self.render_frame,
576 &mut service.eviction_epoch,
577 );
578 self.rendered_scroll_offset = self.scroll_offset;
579 self.rendered_zoom = self.zoom;
580 self.rendered_raster_scale = self.raster_scale;
581 apply_zoom(&mut self.render_frame, self.zoom);
582 &self.render_frame
583 }
584
585 pub fn render_block_only(
598 &mut self,
599 service: &mut TextFontService,
600 block_id: usize,
601 ) -> &RenderFrame {
602 if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
603 || (self.zoom - self.rendered_zoom).abs() > 0.001
604 || (self.raster_scale - self.rendered_raster_scale).abs() > 0.001
605 {
606 return self.render(service);
607 }
608
609 if service.eviction_epoch != self.render_frame.atlas_eviction_epoch {
615 return self.render(service);
616 }
617
618 if !self.flow_layout.blocks.contains_key(&block_id) {
619 let in_table = self.flow_layout.tables.values().any(|table| {
620 table
621 .cell_layouts
622 .iter()
623 .any(|c| c.blocks.iter().any(|b| b.block_id == block_id))
624 });
625 if in_table {
626 return self.render(service);
627 }
628 let in_frame = self
629 .flow_layout
630 .frames
631 .values()
632 .any(|frame| crate::layout::flow::frame_contains_block(frame, block_id));
633 if in_frame {
634 return self.render(service);
635 }
636 }
637
638 if let Some(block) = self.flow_layout.blocks.get(&block_id) {
639 let old_height = self
640 .render_frame
641 .block_heights
642 .get(&block_id)
643 .copied()
644 .unwrap_or(block.height);
645 if (block.height - old_height).abs() > 0.001 {
646 return self.render(service);
647 }
648 }
649
650 let effective_vw = self.viewport_width / self.zoom;
651 let effective_vh = self.viewport_height / self.zoom;
652 let scale_factor = service.scale_factor;
653 let mut new_glyphs = Vec::new();
654 let mut new_images = Vec::new();
655 let mut new_keys: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
656 if let Some(block) = self.flow_layout.blocks.get(&block_id) {
657 let mut tmp = RenderFrame::new();
658 crate::render::frame::render_block_at_offset(
659 block,
660 0.0,
661 0.0,
662 &service.font_registry,
663 &mut service.atlas,
664 &mut service.glyph_cache,
665 &mut service.scale_context,
666 self.scroll_offset,
667 effective_vh,
668 self.text_color,
669 scale_factor,
670 self.raster_scale,
671 &mut tmp,
672 &mut new_keys,
673 &mut service.eviction_epoch,
674 );
675 new_glyphs = tmp.glyphs;
676 new_images = tmp.images;
677 }
678
679 let new_decos = if let Some(block) = self.flow_layout.blocks.get(&block_id) {
680 crate::render::decoration::generate_block_decorations(
681 block,
682 &service.font_registry,
683 self.scroll_offset,
684 effective_vh,
685 0.0,
686 0.0,
687 effective_vw,
688 self.text_color,
689 scale_factor,
690 )
691 } else {
692 Vec::new()
693 };
694
695 if let Some(entry) = self
696 .render_frame
697 .block_glyphs
698 .iter_mut()
699 .find(|(id, _)| *id == block_id)
700 {
701 entry.1 = new_glyphs;
702 }
703 if let Some(entry) = self
704 .render_frame
705 .block_images
706 .iter_mut()
707 .find(|(id, _)| *id == block_id)
708 {
709 entry.1 = new_images;
710 }
711 if let Some(entry) = self
712 .render_frame
713 .block_decorations
714 .iter_mut()
715 .find(|(id, _)| *id == block_id)
716 {
717 entry.1 = new_decos;
718 }
719 if let Some(entry) = self
720 .render_frame
721 .block_glyph_keys
722 .iter_mut()
723 .find(|(id, _)| *id == block_id)
724 {
725 entry.1 = new_keys;
726 }
727
728 self.rebuild_flat_frame(service);
729 apply_zoom(&mut self.render_frame, self.zoom);
730 &self.render_frame
731 }
732
733 pub fn render_cursor_only(&mut self, service: &mut TextFontService) -> &RenderFrame {
741 if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
742 || (self.zoom - self.rendered_zoom).abs() > 0.001
743 {
744 return self.render(service);
745 }
746
747 if service.eviction_epoch != self.render_frame.atlas_eviction_epoch {
752 return self.render(service);
753 }
754
755 service.touch_glyphs(&self.render_frame.glyph_keys);
763
764 self.render_frame.decorations.retain(|d| {
765 !matches!(
766 d.kind,
767 DecorationKind::Cursor | DecorationKind::Selection | DecorationKind::CellSelection
768 )
769 });
770
771 let effective_vw = self.viewport_width / self.zoom;
772 let effective_vh = self.viewport_height / self.zoom;
773 let mut cursor_decos = crate::render::cursor::generate_cursor_decorations(
774 &self.flow_layout,
775 &self.cursors,
776 self.scroll_offset,
777 self.cursor_color,
778 self.selection_color,
779 effective_vw,
780 effective_vh,
781 );
782 apply_zoom_decorations(&mut cursor_decos, self.zoom);
783 self.render_frame.decorations.extend(cursor_decos);
784
785 &self.render_frame
786 }
787
788 fn rebuild_flat_frame(&mut self, service: &mut TextFontService) {
789 self.render_frame.glyphs.clear();
790 self.render_frame.images.clear();
791 self.render_frame.decorations.clear();
792 self.render_frame.glyph_keys.clear();
793 for (_, glyphs) in &self.render_frame.block_glyphs {
794 self.render_frame.glyphs.extend_from_slice(glyphs);
795 }
796 for (_, images) in &self.render_frame.block_images {
797 self.render_frame.images.extend_from_slice(images);
798 }
799 for (_, decos) in &self.render_frame.block_decorations {
800 self.render_frame.decorations.extend_from_slice(decos);
801 }
802 for (_, keys) in &self.render_frame.block_glyph_keys {
803 self.render_frame.glyph_keys.extend_from_slice(keys);
804 }
805 service.touch_glyphs(&self.render_frame.glyph_keys);
812
813 for item in &self.flow_layout.flow_order {
814 match item {
815 FlowItem::Table { table_id, .. } => {
816 if let Some(table) = self.flow_layout.tables.get(table_id) {
817 let decos = crate::layout::table::generate_table_decorations(
818 table,
819 self.scroll_offset,
820 );
821 self.render_frame.decorations.extend(decos);
822 }
823 }
824 FlowItem::Frame { frame_id, .. } => {
825 if let Some(frame) = self.flow_layout.frames.get(frame_id) {
826 crate::render::frame::append_frame_border_decorations(
827 frame,
828 self.scroll_offset,
829 &mut self.render_frame.decorations,
830 );
831 }
832 }
833 FlowItem::Block { .. } => {}
834 }
835 }
836
837 let effective_vw = self.viewport_width / self.zoom;
838 let effective_vh = self.viewport_height / self.zoom;
839 let cursor_decos = crate::render::cursor::generate_cursor_decorations(
840 &self.flow_layout,
841 &self.cursors,
842 self.scroll_offset,
843 self.cursor_color,
844 self.selection_color,
845 effective_vw,
846 effective_vh,
847 );
848 self.render_frame.decorations.extend(cursor_decos);
849
850 self.render_frame.atlas_dirty = service.atlas.dirty;
851 self.render_frame.atlas_width = service.atlas.width;
852 self.render_frame.atlas_height = service.atlas.height;
853 if service.atlas.dirty {
854 let pixels = &service.atlas.pixels;
855 let needed = (service.atlas.width * service.atlas.height * 4) as usize;
856 self.render_frame.atlas_pixels.resize(needed, 0);
857 let copy_len = needed.min(pixels.len());
858 self.render_frame.atlas_pixels[..copy_len].copy_from_slice(&pixels[..copy_len]);
859 service.atlas.dirty = false;
860 }
861 }
862
863 pub fn layout_single_line(
879 &mut self,
880 service: &mut TextFontService,
881 text: &str,
882 format: &TextFormat,
883 max_width: Option<f32>,
884 raster_scale: f32,
885 ) -> SingleLineResult {
886 let empty = SingleLineResult {
887 width: 0.0,
888 height: 0.0,
889 baseline: 0.0,
890 underline_offset: 0.0,
891 underline_thickness: 0.0,
892 glyphs: Vec::new(),
893 glyph_keys: Vec::new(),
894 spans: Vec::new(),
895 };
896
897 if text.is_empty() {
898 return empty;
899 }
900
901 let font_point_size = format.font_size.map(|s| s as u32);
902 let resolved = match resolve_font(
903 &service.font_registry,
904 format.font_family.as_deref(),
905 format.font_weight,
906 format.font_bold,
907 format.font_italic,
908 font_point_size,
909 service.scale_factor,
910 ) {
911 Some(r) => r,
912 None => return empty,
913 };
914
915 let metrics = match font_metrics_px(&service.font_registry, &resolved) {
916 Some(m) => m,
917 None => return empty,
918 };
919 let line_height = metrics.ascent + metrics.descent + metrics.leading;
920 let baseline = metrics.ascent;
921
922 let features = to_harfrust_features(&format.features);
923 let runs: Vec<_> = bidi_runs(text)
924 .into_iter()
925 .filter_map(|br| {
926 let slice = text.get(br.byte_range.clone())?;
927 shape_text_with_fallback(
928 &service.font_registry,
929 &resolved,
930 slice,
931 br.byte_range.start,
932 br.direction,
933 &features,
934 )
935 })
936 .collect();
937
938 if runs.is_empty() {
939 return empty;
940 }
941
942 let total_advance: f32 = runs.iter().map(|r| r.advance_width).sum();
943
944 let (truncate_at_visual_index, final_width, ellipsis_run) = if let Some(max_w) = max_width
945 && total_advance > max_w
946 {
947 let ellipsis_run = shape_text(&service.font_registry, &resolved, "\u{2026}", 0);
948 let ellipsis_width = ellipsis_run
949 .as_ref()
950 .map(|r| r.advance_width)
951 .unwrap_or(0.0);
952 let budget = (max_w - ellipsis_width).max(0.0);
953
954 let mut used = 0.0f32;
955 let mut count = 0usize;
956 'outer: for run in &runs {
957 for g in &run.glyphs {
958 if used + g.x_advance > budget {
959 break 'outer;
960 }
961 used += g.x_advance;
962 count += 1;
963 }
964 }
965
966 (Some(count), used + ellipsis_width, ellipsis_run)
967 } else {
968 (None, total_advance, None)
969 };
970
971 let text_color = format.color.unwrap_or(self.text_color);
972 let glyph_capacity: usize = runs.iter().map(|r| r.glyphs.len()).sum();
973 let mut quads = Vec::with_capacity(glyph_capacity + 1);
974 let mut keys = Vec::with_capacity(glyph_capacity + 1);
975 let mut pen_x = 0.0f32;
976 let mut emitted = 0usize;
977
978 'emit: for run in &runs {
979 for glyph in &run.glyphs {
980 if let Some(limit) = truncate_at_visual_index
981 && emitted >= limit
982 {
983 break 'emit;
984 }
985 rasterize_glyph_quad(
986 service,
987 glyph,
988 run,
989 pen_x,
990 baseline,
991 text_color,
992 raster_scale,
993 &mut quads,
994 &mut keys,
995 );
996 pen_x += glyph.x_advance;
997 emitted += 1;
998 }
999 }
1000
1001 if let Some(ref e_run) = ellipsis_run {
1002 for glyph in &e_run.glyphs {
1003 rasterize_glyph_quad(
1004 service,
1005 glyph,
1006 e_run,
1007 pen_x,
1008 baseline,
1009 text_color,
1010 raster_scale,
1011 &mut quads,
1012 &mut keys,
1013 );
1014 pen_x += glyph.x_advance;
1015 }
1016 }
1017
1018 SingleLineResult {
1019 width: final_width,
1020 height: line_height,
1021 baseline,
1022 underline_offset: metrics.underline_offset,
1023 underline_thickness: metrics.stroke_size,
1024 glyphs: quads,
1025 glyph_keys: keys,
1026 spans: Vec::new(),
1027 }
1028 }
1029
1030 pub fn layout_paragraph(
1044 &mut self,
1045 service: &mut TextFontService,
1046 text: &str,
1047 format: &TextFormat,
1048 max_width: f32,
1049 max_lines: Option<usize>,
1050 raster_scale: f32,
1051 ) -> ParagraphResult {
1052 let empty = ParagraphResult {
1053 width: 0.0,
1054 height: 0.0,
1055 baseline_first: 0.0,
1056 line_count: 0,
1057 line_height: 0.0,
1058 underline_offset: 0.0,
1059 underline_thickness: 0.0,
1060 glyphs: Vec::new(),
1061 glyph_keys: Vec::new(),
1062 spans: Vec::new(),
1063 };
1064
1065 if text.is_empty() || max_width <= 0.0 {
1066 return empty;
1067 }
1068
1069 let font_point_size = format.font_size.map(|s| s as u32);
1070 let resolved = match resolve_font(
1071 &service.font_registry,
1072 format.font_family.as_deref(),
1073 format.font_weight,
1074 format.font_bold,
1075 format.font_italic,
1076 font_point_size,
1077 service.scale_factor,
1078 ) {
1079 Some(r) => r,
1080 None => return empty,
1081 };
1082
1083 let metrics = match font_metrics_px(&service.font_registry, &resolved) {
1084 Some(m) => m,
1085 None => return empty,
1086 };
1087
1088 let features = to_harfrust_features(&format.features);
1089 let runs: Vec<_> = bidi_runs(text)
1090 .into_iter()
1091 .filter_map(|br| {
1092 let slice = text.get(br.byte_range.clone())?;
1093 shape_text_with_fallback(
1094 &service.font_registry,
1095 &resolved,
1096 slice,
1097 br.byte_range.start,
1098 br.direction,
1099 &features,
1100 )
1101 })
1102 .collect();
1103
1104 if runs.is_empty() {
1105 return empty;
1106 }
1107
1108 let hyphenator = format.hyphenation.and_then(|h| {
1109 shape_text(&service.font_registry, &resolved, "-", 0)
1110 .and_then(|r| r.glyphs.into_iter().next())
1111 .map(|glyph| Hyphenator {
1112 glyph,
1113 language: h.language,
1114 })
1115 });
1116 let lines = break_into_lines(
1117 runs,
1118 text,
1119 max_width,
1120 Alignment::Left,
1121 0.0,
1122 &metrics,
1123 hyphenator,
1124 );
1125
1126 let line_count = match max_lines {
1127 Some(n) => lines.len().min(n),
1128 None => lines.len(),
1129 };
1130
1131 let text_color = format.color.unwrap_or(self.text_color);
1132 let mut quads: Vec<GlyphQuad> = Vec::new();
1133 let mut keys: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
1134 let mut y_top = 0.0f32;
1135 let mut max_line_width = 0.0f32;
1136 let baseline_first = metrics.ascent;
1137
1138 for line in lines.iter().take(line_count) {
1139 if line.width > max_line_width {
1140 max_line_width = line.width;
1141 }
1142 let baseline_y = y_top + metrics.ascent;
1143 for run in &line.runs {
1144 let mut pen_x = run.x;
1145 let run_copy = run.shaped_run.clone();
1146 for glyph in &run_copy.glyphs {
1147 rasterize_glyph_quad(
1148 service,
1149 glyph,
1150 &run_copy,
1151 pen_x,
1152 baseline_y,
1153 text_color,
1154 raster_scale,
1155 &mut quads,
1156 &mut keys,
1157 );
1158 pen_x += glyph.x_advance;
1159 }
1160 }
1161 y_top += metrics.ascent + metrics.descent + metrics.leading;
1162 }
1163
1164 let line_height = metrics.ascent + metrics.descent + metrics.leading;
1165 ParagraphResult {
1166 width: max_line_width,
1167 height: y_top,
1168 baseline_first,
1169 line_count,
1170 line_height,
1171 underline_offset: metrics.underline_offset,
1172 underline_thickness: metrics.stroke_size,
1173 glyphs: quads,
1174 glyph_keys: keys,
1175 spans: Vec::new(),
1176 }
1177 }
1178
1179 pub fn layout_single_line_markup(
1185 &mut self,
1186 service: &mut TextFontService,
1187 markup: &InlineMarkup,
1188 format: &TextFormat,
1189 max_width: Option<f32>,
1190 raster_scale: f32,
1191 ) -> SingleLineResult {
1192 if markup.spans.is_empty() {
1193 return SingleLineResult {
1194 width: 0.0,
1195 height: 0.0,
1196 baseline: 0.0,
1197 underline_offset: 0.0,
1198 underline_thickness: 0.0,
1199 glyphs: Vec::new(),
1200 glyph_keys: Vec::new(),
1201 spans: Vec::new(),
1202 };
1203 }
1204
1205 let per_span: Vec<(SingleLineResult, &crate::layout::inline_markup::InlineSpan)> = markup
1206 .spans
1207 .iter()
1208 .map(|sp| {
1209 let fmt = merge_format(format, sp.attrs);
1210 let r = if sp.text.is_empty() {
1211 SingleLineResult {
1212 width: 0.0,
1213 height: 0.0,
1214 baseline: 0.0,
1215 underline_offset: 0.0,
1216 underline_thickness: 0.0,
1217 glyphs: Vec::new(),
1218 glyph_keys: Vec::new(),
1219 spans: Vec::new(),
1220 }
1221 } else {
1222 self.layout_single_line(service, &sp.text, &fmt, None, raster_scale)
1223 };
1224 (r, sp)
1225 })
1226 .collect();
1227
1228 let total_width: f32 = per_span.iter().map(|(r, _)| r.width).sum();
1229 let line_height = per_span
1230 .iter()
1231 .map(|(r, _)| r.height)
1232 .fold(0.0f32, f32::max);
1233 let baseline = per_span
1234 .iter()
1235 .map(|(r, _)| r.baseline)
1236 .fold(0.0f32, f32::max);
1237 let (underline_offset, underline_thickness) = per_span
1241 .iter()
1242 .map(|(r, _)| (r.underline_offset, r.underline_thickness))
1243 .find(|(_, t)| *t > 0.0)
1244 .unwrap_or((0.0, 0.0));
1245
1246 let truncate = match max_width {
1247 Some(mw) if total_width > mw => Some(mw),
1248 _ => None,
1249 };
1250
1251 let mut glyphs: Vec<GlyphQuad> = Vec::new();
1252 let mut all_keys: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
1253 let mut spans_out: Vec<LaidOutSpan> = Vec::new();
1254 let mut pen_x: f32 = 0.0;
1255 let effective_width = truncate.unwrap_or(total_width);
1256
1257 for (r, sp) in &per_span {
1258 let remaining = (effective_width - pen_x).max(0.0);
1259 let span_visible_width = r.width.min(remaining);
1260 if span_visible_width <= 0.0 && r.width > 0.0 {
1261 spans_out.push(LaidOutSpan {
1262 kind: if let Some(url) = sp.link_url.clone() {
1263 LaidOutSpanKind::Link { url }
1264 } else {
1265 LaidOutSpanKind::Text
1266 },
1267 line_index: 0,
1268 rect: [pen_x, 0.0, 0.0, line_height],
1269 byte_range: sp.byte_range.clone(),
1270 });
1271 continue;
1272 }
1273
1274 for (gi, g) in r.glyphs.iter().enumerate() {
1275 let g_right = pen_x + g.screen[0] + g.screen[2];
1276 if g_right > effective_width + 0.5 {
1277 break;
1278 }
1279 let mut gq = g.clone();
1280 gq.screen[0] += pen_x;
1281 glyphs.push(gq);
1282 if let Some(k) = r.glyph_keys.get(gi) {
1283 all_keys.push(*k);
1284 }
1285 }
1286
1287 spans_out.push(LaidOutSpan {
1288 kind: if let Some(url) = sp.link_url.clone() {
1289 LaidOutSpanKind::Link { url }
1290 } else {
1291 LaidOutSpanKind::Text
1292 },
1293 line_index: 0,
1294 rect: [pen_x, 0.0, span_visible_width, line_height],
1295 byte_range: sp.byte_range.clone(),
1296 });
1297
1298 pen_x += r.width;
1299 if truncate.is_some() && pen_x >= effective_width {
1300 break;
1301 }
1302 }
1303
1304 SingleLineResult {
1305 width: effective_width,
1306 height: line_height,
1307 baseline,
1308 underline_offset,
1309 underline_thickness,
1310 glyphs,
1311 glyph_keys: all_keys,
1312 spans: spans_out,
1313 }
1314 }
1315
1316 pub fn layout_paragraph_markup(
1321 &mut self,
1322 service: &mut TextFontService,
1323 markup: &InlineMarkup,
1324 format: &TextFormat,
1325 max_width: f32,
1326 max_lines: Option<usize>,
1327 raster_scale: f32,
1328 ) -> ParagraphResult {
1329 let empty = ParagraphResult {
1330 width: 0.0,
1331 height: 0.0,
1332 baseline_first: 0.0,
1333 line_count: 0,
1334 line_height: 0.0,
1335 underline_offset: 0.0,
1336 underline_thickness: 0.0,
1337 glyphs: Vec::new(),
1338 glyph_keys: Vec::new(),
1339 spans: Vec::new(),
1340 };
1341
1342 if markup.spans.is_empty() || max_width <= 0.0 {
1343 return empty;
1344 }
1345
1346 let mut flat = String::new();
1347 let mut span_flat_offsets: Vec<usize> = Vec::with_capacity(markup.spans.len());
1348 for sp in &markup.spans {
1349 span_flat_offsets.push(flat.len());
1350 flat.push_str(&sp.text);
1351 }
1352 if flat.is_empty() {
1353 return empty;
1354 }
1355
1356 let base_point_size = format.font_size.map(|s| s as u32);
1357 let base_resolved = match resolve_font(
1358 &service.font_registry,
1359 format.font_family.as_deref(),
1360 format.font_weight,
1361 format.font_bold,
1362 format.font_italic,
1363 base_point_size,
1364 service.scale_factor,
1365 ) {
1366 Some(r) => r,
1367 None => return empty,
1368 };
1369 let metrics = match font_metrics_px(&service.font_registry, &base_resolved) {
1370 Some(m) => m,
1371 None => return empty,
1372 };
1373
1374 let mut all_runs: Vec<ShapedRun> = Vec::new();
1375 for (span_idx, sp) in markup.spans.iter().enumerate() {
1376 if sp.text.is_empty() {
1377 continue;
1378 }
1379 let fmt = merge_format(format, sp.attrs);
1380 let span_point_size = fmt.font_size.map(|s| s as u32);
1381 let Some(resolved) = resolve_font(
1382 &service.font_registry,
1383 fmt.font_family.as_deref(),
1384 fmt.font_weight,
1385 fmt.font_bold,
1386 fmt.font_italic,
1387 span_point_size,
1388 service.scale_factor,
1389 ) else {
1390 continue;
1391 };
1392
1393 let flat_start = span_flat_offsets[span_idx];
1394 let features = to_harfrust_features(&fmt.features);
1395 for br in bidi_runs(&sp.text) {
1396 let slice = match sp.text.get(br.byte_range.clone()) {
1397 Some(s) => s,
1398 None => continue,
1399 };
1400 let Some(mut run) = shape_text_with_fallback(
1401 &service.font_registry,
1402 &resolved,
1403 slice,
1404 flat_start + br.byte_range.start,
1405 br.direction,
1406 &features,
1407 ) else {
1408 continue;
1409 };
1410 if let Some(url) = sp.link_url.as_ref() {
1411 run.is_link = true;
1412 run.anchor_href = Some(url.clone());
1413 }
1414 all_runs.push(run);
1415 }
1416 }
1417
1418 if all_runs.is_empty() {
1419 return empty;
1420 }
1421
1422 let hyphenator = format.hyphenation.and_then(|h| {
1423 shape_text(&service.font_registry, &base_resolved, "-", 0)
1424 .and_then(|r| r.glyphs.into_iter().next())
1425 .map(|glyph| Hyphenator {
1426 glyph,
1427 language: h.language,
1428 })
1429 });
1430 let lines = break_into_lines(
1431 all_runs,
1432 &flat,
1433 max_width,
1434 Alignment::Left,
1435 0.0,
1436 &metrics,
1437 hyphenator,
1438 );
1439
1440 let line_count = match max_lines {
1441 Some(n) => lines.len().min(n),
1442 None => lines.len(),
1443 };
1444
1445 let text_color = format.color.unwrap_or(self.text_color);
1446 let mut glyphs_out: Vec<GlyphQuad> = Vec::new();
1447 let mut keys_out: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
1448 let mut spans_out: Vec<LaidOutSpan> = Vec::new();
1449 let line_height = metrics.ascent + metrics.descent + metrics.leading;
1450 let mut y_top: f32 = 0.0;
1451 let mut max_line_width: f32 = 0.0;
1452 let baseline_first = metrics.ascent;
1453
1454 for (line_idx, line) in lines.iter().take(line_count).enumerate() {
1455 if line.width > max_line_width {
1456 max_line_width = line.width;
1457 }
1458 let baseline_y = y_top + metrics.ascent;
1459
1460 for pr in &line.runs {
1461 let run_copy = pr.shaped_run.clone();
1462 let mut pen_x = pr.x;
1463 for glyph in &run_copy.glyphs {
1464 rasterize_glyph_quad(
1465 service,
1466 glyph,
1467 &run_copy,
1468 pen_x,
1469 baseline_y,
1470 text_color,
1471 raster_scale,
1472 &mut glyphs_out,
1473 &mut keys_out,
1474 );
1475 pen_x += glyph.x_advance;
1476 }
1477
1478 if pr.decorations.is_link
1479 && let Some(url) = pr.decorations.anchor_href.clone()
1480 {
1481 let width = pr.shaped_run.advance_width;
1482 spans_out.push(LaidOutSpan {
1483 kind: LaidOutSpanKind::Link { url },
1484 line_index: line_idx,
1485 rect: [pr.x, y_top, width, line_height],
1486 byte_range: pr.shaped_run.text_range.clone(),
1487 });
1488 }
1489 }
1490
1491 y_top += line_height;
1492 }
1493
1494 ParagraphResult {
1495 width: max_line_width,
1496 height: y_top,
1497 baseline_first,
1498 line_count,
1499 line_height,
1500 underline_offset: metrics.underline_offset,
1501 underline_thickness: metrics.stroke_size,
1502 glyphs: glyphs_out,
1503 glyph_keys: keys_out,
1504 spans: spans_out,
1505 }
1506 }
1507
1508 pub fn hit_test(&self, x: f32, y: f32) -> Option<HitTestResult> {
1515 crate::render::hit_test::hit_test(
1516 &self.flow_layout,
1517 self.scroll_offset,
1518 x / self.zoom,
1519 y / self.zoom,
1520 )
1521 }
1522
1523 pub fn character_geometry(
1532 &self,
1533 block_id: usize,
1534 char_start: usize,
1535 char_end: usize,
1536 ) -> Vec<CharacterGeometry> {
1537 if char_start >= char_end {
1538 return Vec::new();
1539 }
1540 let block = match self.flow_layout.blocks.get(&block_id) {
1541 Some(b) => b,
1542 None => return Vec::new(),
1543 };
1544
1545 let mut absolute: Vec<(usize, f32)> = Vec::with_capacity(char_end - char_start);
1546 for line in &block.lines {
1547 if line.char_range.end <= char_start || line.char_range.start >= char_end {
1548 continue;
1549 }
1550 let local_start = char_start.max(line.char_range.start);
1551 let local_end = char_end.min(line.char_range.end);
1552 for c in local_start..local_end {
1553 let x = line.x_for_offset(c);
1554 absolute.push((c, x));
1555 }
1556 if local_end == char_end {
1557 let x_end = line.x_for_offset(local_end);
1558 absolute.push((local_end, x_end));
1559 }
1560 }
1561
1562 if absolute.is_empty() {
1563 return Vec::new();
1564 }
1565
1566 absolute.sort_by_key(|(c, _)| *c);
1567
1568 let base_x = absolute.first().map(|(_, x)| *x).unwrap_or(0.0);
1569 let mut out: Vec<CharacterGeometry> = Vec::with_capacity(absolute.len());
1570 for window in absolute.windows(2) {
1571 let (c, x) = window[0];
1572 let (_, x_next) = window[1];
1573 if c >= char_end {
1574 break;
1575 }
1576 out.push(CharacterGeometry {
1577 position: x - base_x,
1578 width: (x_next - x).max(0.0),
1579 });
1580 }
1581 out
1582 }
1583
1584 pub fn caret_rect(&self, position: usize, affinity: crate::types::CursorAffinity) -> [f32; 4] {
1594 let mut rect = crate::render::hit_test::caret_rect(
1595 &self.flow_layout,
1596 self.scroll_offset,
1597 position,
1598 affinity,
1599 );
1600 rect[0] *= self.zoom;
1601 rect[1] *= self.zoom;
1602 rect[2] *= self.zoom;
1603 rect[3] *= self.zoom;
1604 rect
1605 }
1606
1607 pub fn set_cursor(&mut self, cursor: &CursorDisplay) {
1611 self.cursors = vec![CursorDisplay {
1612 position: cursor.position,
1613 anchor: cursor.anchor,
1614 affinity: cursor.affinity,
1615 visible: cursor.visible,
1616 selected_cells: cursor.selected_cells.clone(),
1617 }];
1618 }
1619
1620 pub fn set_cursors(&mut self, cursors: &[CursorDisplay]) {
1624 self.cursors = cursors
1625 .iter()
1626 .map(|c| CursorDisplay {
1627 position: c.position,
1628 anchor: c.anchor,
1629 affinity: c.affinity,
1630 visible: c.visible,
1631 selected_cells: c.selected_cells.clone(),
1632 })
1633 .collect();
1634 }
1635
1636 pub fn set_selection_color(&mut self, color: [f32; 4]) {
1639 self.selection_color = color;
1640 }
1641
1642 pub fn set_cursor_color(&mut self, color: [f32; 4]) {
1644 self.cursor_color = color;
1645 }
1646
1647 pub fn set_text_color(&mut self, color: [f32; 4]) {
1650 self.text_color = color;
1651 }
1652
1653 pub fn text_color(&self) -> [f32; 4] {
1655 self.text_color
1656 }
1657
1658 pub fn set_code_block_background(&mut self, color: [f32; 4]) {
1665 self.code_block_background = color;
1666 }
1667
1668 pub fn code_block_background(&self) -> [f32; 4] {
1670 self.code_block_background
1671 }
1672
1673 pub fn set_hyphenate_justified(&mut self, enabled: bool) {
1678 self.hyphenate_justified = enabled;
1679 }
1680
1681 pub fn hyphenate_justified(&self) -> bool {
1683 self.hyphenate_justified
1684 }
1685
1686 pub fn set_code_block_foreground(&mut self, color: Option<[f32; 4]>) {
1691 self.code_block_foreground = color;
1692 }
1693
1694 pub fn code_block_foreground(&self) -> Option<[f32; 4]> {
1696 self.code_block_foreground
1697 }
1698
1699 pub fn set_echo_char(&mut self, echo: Option<char>) {
1714 self.echo_char = echo;
1715 }
1716
1717 pub fn echo_char(&self) -> Option<char> {
1719 self.echo_char
1720 }
1721
1722 pub fn block_visual_info(&self, block_id: usize) -> Option<BlockVisualInfo> {
1727 let block = self.flow_layout.blocks.get(&block_id)?;
1728 Some(BlockVisualInfo {
1729 block_id,
1730 y: block.y,
1731 height: block.height,
1732 })
1733 }
1734
1735 pub fn is_block_in_table(&self, block_id: usize) -> bool {
1737 self.flow_layout.tables.values().any(|table| {
1738 table
1739 .cell_layouts
1740 .iter()
1741 .any(|cell| cell.blocks.iter().any(|b| b.block_id == block_id))
1742 })
1743 }
1744
1745 pub fn scroll_to_position(&mut self, position: usize) -> f32 {
1750 let rect = crate::render::hit_test::caret_rect(
1751 &self.flow_layout,
1752 self.scroll_offset,
1753 position,
1754 crate::types::CursorAffinity::Downstream,
1755 );
1756 let target_y = rect[1] + self.scroll_offset - self.viewport_height / (3.0 * self.zoom);
1757 self.scroll_offset = target_y.max(0.0);
1758 self.scroll_offset
1759 }
1760
1761 pub fn ensure_caret_visible(&mut self) -> Option<f32> {
1765 if self.cursors.is_empty() {
1766 return None;
1767 }
1768 let pos = self.cursors[0].position;
1769 let affinity = self.cursors[0].affinity;
1770 let rect = crate::render::hit_test::caret_rect(
1771 &self.flow_layout,
1772 self.scroll_offset,
1773 pos,
1774 affinity,
1775 );
1776 let caret_screen_y = rect[1];
1777 let caret_screen_bottom = caret_screen_y + rect[3];
1778 let effective_vh = self.viewport_height / self.zoom;
1779 let margin = 10.0 / self.zoom;
1780 let old_offset = self.scroll_offset;
1781
1782 if caret_screen_y < 0.0 {
1783 self.scroll_offset += caret_screen_y - margin;
1784 self.scroll_offset = self.scroll_offset.max(0.0);
1785 } else if caret_screen_bottom > effective_vh {
1786 self.scroll_offset += caret_screen_bottom - effective_vh + margin;
1787 }
1788
1789 if (self.scroll_offset - old_offset).abs() > 0.001 {
1790 Some(self.scroll_offset)
1791 } else {
1792 None
1793 }
1794 }
1795}
1796
1797impl Default for DocumentFlow {
1798 fn default() -> Self {
1799 Self::new()
1800 }
1801}
1802
1803#[cfg(feature = "text-document")]
1804enum FlowItemKind {
1805 Block(BlockLayoutParams),
1806 Table(TableLayoutParams),
1807 Frame(FrameLayoutParams),
1808}
1809
1810#[allow(clippy::too_many_arguments)]
1823fn rasterize_glyph_quad(
1824 service: &mut TextFontService,
1825 glyph: &ShapedGlyph,
1826 run: &ShapedRun,
1827 pen_x: f32,
1828 baseline: f32,
1829 text_color: [f32; 4],
1830 raster_scale: f32,
1831 quads: &mut Vec<GlyphQuad>,
1832 glyph_keys: &mut Vec<crate::atlas::cache::GlyphCacheKey>,
1833) {
1834 use crate::atlas::cache::GlyphCacheKey;
1835 use crate::atlas::rasterizer::rasterize_glyph;
1836
1837 if glyph.glyph_id == 0 {
1838 return;
1839 }
1840
1841 let entry = match service.font_registry.get(glyph.font_face_id) {
1842 Some(e) => e,
1843 None => return,
1844 };
1845
1846 let raster_scale = if raster_scale > 0.0 {
1847 raster_scale
1848 } else {
1849 1.0
1850 };
1851 let hinted = raster_scale == 1.0;
1852 let sf = service.scale_factor.max(f32::MIN_POSITIVE);
1853 let inv_total = 1.0 / (sf * raster_scale);
1854 let physical_size_px = run.size_px * sf * raster_scale;
1855 let cache_key = GlyphCacheKey::with_weight(
1856 glyph.font_face_id,
1857 glyph.glyph_id,
1858 physical_size_px,
1859 run.weight as u32,
1860 hinted,
1861 );
1862
1863 if service.glyph_cache.peek(&cache_key).is_none()
1864 && let Some(image) = rasterize_glyph(
1865 &mut service.scale_context,
1866 entry.bytes(),
1867 entry.face_index,
1868 entry.swash_cache_key,
1869 glyph.glyph_id,
1870 physical_size_px,
1871 run.weight as u32,
1872 hinted,
1873 )
1874 && image.width > 0
1875 && image.height > 0
1876 {
1877 let (alloc, evicted) = crate::atlas::allocate_or_evict(
1878 &mut service.atlas,
1879 &mut service.glyph_cache,
1880 image.width,
1881 image.height,
1882 );
1883 if evicted {
1884 service.eviction_epoch = service.eviction_epoch.wrapping_add(1);
1885 }
1886 if let Some(alloc) = alloc {
1887 let rect = alloc.rectangle;
1888 let atlas_x = rect.min.x as u32;
1889 let atlas_y = rect.min.y as u32;
1890 if image.is_color {
1891 service
1892 .atlas
1893 .blit_rgba(atlas_x, atlas_y, image.width, image.height, &image.data);
1894 } else {
1895 service
1896 .atlas
1897 .blit_mask(atlas_x, atlas_y, image.width, image.height, &image.data);
1898 }
1899 service.glyph_cache.insert(
1900 cache_key,
1901 crate::atlas::cache::CachedGlyph {
1902 alloc_id: alloc.id,
1903 atlas_x,
1904 atlas_y,
1905 width: image.width,
1906 height: image.height,
1907 placement_left: image.placement_left,
1908 placement_top: image.placement_top,
1909 is_color: image.is_color,
1910 last_used: 0,
1911 },
1912 );
1913 }
1914 }
1915
1916 if let Some(cached) = service.glyph_cache.get(&cache_key) {
1917 let logical_w = cached.width as f32 * inv_total;
1918 let logical_h = cached.height as f32 * inv_total;
1919 let logical_left = cached.placement_left as f32 * inv_total;
1920 let logical_top = cached.placement_top as f32 * inv_total;
1921 let screen_x = pen_x + glyph.x_offset + logical_left;
1922 let screen_y = baseline - glyph.y_offset - logical_top;
1923 let color = if cached.is_color {
1924 [1.0, 1.0, 1.0, 1.0]
1925 } else {
1926 text_color
1927 };
1928 quads.push(GlyphQuad {
1929 screen: [screen_x, screen_y, logical_w, logical_h],
1930 atlas: [
1931 cached.atlas_x as f32,
1932 cached.atlas_y as f32,
1933 cached.width as f32,
1934 cached.height as f32,
1935 ],
1936 color,
1937 is_color: cached.is_color,
1938 });
1939 glyph_keys.push(cache_key);
1940 }
1941}
1942
1943fn apply_zoom(frame: &mut RenderFrame, zoom: f32) {
1945 if (zoom - 1.0).abs() <= f32::EPSILON {
1946 return;
1947 }
1948 for q in &mut frame.glyphs {
1949 q.screen[0] *= zoom;
1950 q.screen[1] *= zoom;
1951 q.screen[2] *= zoom;
1952 q.screen[3] *= zoom;
1953 }
1954 for q in &mut frame.images {
1955 q.screen[0] *= zoom;
1956 q.screen[1] *= zoom;
1957 q.screen[2] *= zoom;
1958 q.screen[3] *= zoom;
1959 }
1960 apply_zoom_decorations(&mut frame.decorations, zoom);
1961}
1962
1963fn apply_zoom_decorations(decorations: &mut [DecorationRect], zoom: f32) {
1965 if (zoom - 1.0).abs() <= f32::EPSILON {
1966 return;
1967 }
1968 for d in decorations.iter_mut() {
1969 d.rect[0] *= zoom;
1970 d.rect[1] *= zoom;
1971 d.rect[2] *= zoom;
1972 d.rect[3] *= zoom;
1973 }
1974}
1975
1976fn merge_format(base: &TextFormat, attrs: InlineAttrs) -> TextFormat {
1979 let mut fmt = base.clone();
1980 if attrs.is_bold() {
1981 fmt.font_bold = Some(true);
1982 if let Some(w) = fmt.font_weight
1983 && w < 600
1984 {
1985 fmt.font_weight = Some(700);
1986 } else if fmt.font_weight.is_none() {
1987 fmt.font_weight = Some(700);
1988 }
1989 }
1990 if attrs.is_italic() {
1991 fmt.font_italic = Some(true);
1992 }
1993 fmt
1994}
1995
1996#[cfg(test)]
1997mod tests {
1998 use super::*;
1999 use crate::layout::block::{BlockLayoutParams, FragmentParams};
2000 use crate::layout::paragraph::Alignment;
2001 use crate::types::{UnderlineStyle, VerticalAlignment};
2002
2003 const NOTO_SANS: &[u8] = include_bytes!("../test-fonts/NotoSans-Variable.ttf");
2004
2005 fn service() -> TextFontService {
2006 let mut s = TextFontService::new_without_system_fonts();
2008 let face = s.register_font(NOTO_SANS);
2009 s.set_default_font(face, 16.0);
2010 s
2011 }
2012
2013 fn block(id: usize, text: &str) -> BlockLayoutParams {
2014 BlockLayoutParams {
2015 block_id: id,
2016 position: 0,
2017 text: text.to_string(),
2018 fragments: vec![FragmentParams {
2019 text: text.to_string(),
2020 offset: 0,
2021 length: text.len(),
2022 font_family: None,
2023 font_weight: None,
2024 font_bold: None,
2025 font_italic: None,
2026 font_point_size: None,
2027 underline_style: UnderlineStyle::None,
2028 overline: false,
2029 strikeout: false,
2030 is_link: false,
2031 letter_spacing: 0.0,
2032 word_spacing: 0.0,
2033 foreground_color: None,
2034 underline_color: None,
2035 background_color: None,
2036 anchor_href: None,
2037 tooltip: None,
2038 vertical_alignment: VerticalAlignment::Normal,
2039 image_name: None,
2040 image_width: 0.0,
2041 image_height: 0.0,
2042 features: Vec::new(),
2043 }],
2044 alignment: Alignment::Left,
2045 top_margin: 0.0,
2046 bottom_margin: 0.0,
2047 left_margin: 0.0,
2048 right_margin: 0.0,
2049 text_indent: 0.0,
2050 list_marker: String::new(),
2051 list_indent: 0.0,
2052 tab_positions: vec![],
2053 line_height_multiplier: None,
2054 non_breakable_lines: false,
2055 hyphenation: None,
2056 checkbox: None,
2057 background_color: None,
2058 }
2059 }
2060
2061 #[test]
2062 fn relayout_block_returns_no_layout_when_never_laid_out() {
2063 let svc = service();
2064 let mut flow = DocumentFlow::new();
2065 flow.set_viewport(400.0, 200.0);
2066 let err = flow.relayout_block(&svc, &block(1, "Hello")).unwrap_err();
2067 assert_eq!(err, RelayoutError::NoLayout);
2068 }
2069
2070 #[test]
2071 fn relayout_block_returns_scale_dirty_after_scale_factor_change() {
2072 let mut svc = service();
2073 let mut flow = DocumentFlow::new();
2074 flow.set_viewport(400.0, 200.0);
2075 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
2076 assert!(flow.has_layout());
2077
2078 svc.set_scale_factor(2.0);
2080 assert!(flow.layout_dirty_for_scale(&svc));
2081
2082 let err = flow
2083 .relayout_block(&svc, &block(1, "Hello world"))
2084 .unwrap_err();
2085 assert_eq!(err, RelayoutError::ScaleDirty);
2086 }
2087
2088 #[test]
2089 fn relayout_block_succeeds_after_fresh_layout_post_scale_change() {
2090 let mut svc = service();
2091 let mut flow = DocumentFlow::new();
2092 flow.set_viewport(400.0, 200.0);
2093 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
2094
2095 svc.set_scale_factor(2.0);
2096 flow.layout_blocks(&svc, vec![block(1, "Hello")]);
2099 assert!(!flow.layout_dirty_for_scale(&svc));
2100
2101 flow.relayout_block(&svc, &block(1, "Hello world"))
2103 .expect("relayout_block must succeed after a fresh post-scale layout");
2104 }
2105}