diff options
| author | cashmere <cashmere@autistici.org> | 2026-07-28 20:14:51 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-07-28 20:14:51 +0200 |
| commit | 32438ce25f69de99d2523b98237f62cfa90890d7 (patch) | |
| tree | 3a712c347dec0889bdea3328b4c817c2d9392b27 /rust/crates/text/src/layout.rs | |
| parent | f5a82152394fde4d8f740f664ab7737b82861618 (diff) | |
| download | opencut-32438ce25f69de99d2523b98237f62cfa90890d7.tar.gz | |
feat: port domain crates to Rust (session 1 complete)
All green: cargo test --workspace (~300 tests, 0 failures).
- timeline (45): placement, resize/ripple, split, snap, retime,
audio separation, scene helpers
- animation (73): bezier solver, channel sampling, keyframe ops,
split/clamp/clone, param definitions
- commands (9): Command trait, BatchCommand, history, 19 concrete
commands, freeze-frame port 1:1
- audio (22): audible collection, range mixing, mastering limiter,
rms/peak buckets
- text (19): cosmic-text layout/measure/raster, backgrounds,
decorations
- graphics (52): full CSS gradient parser, tiny-skia raster,
4 shape definitions
- effect-defs (16): blur definition, registry, pass resolution
- mask-defs (19): all 9 mask renderers via tiny-skia
- renderer (6): node model + scene builder port
- desktop: GPUI projects screen wired to ProjectStore
- scene: factory (web defaults: v32, Main Track, 1920x1080, 30fps)
Diffstat (limited to 'rust/crates/text/src/layout.rs')
| -rw-r--r-- | rust/crates/text/src/layout.rs | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/rust/crates/text/src/layout.rs b/rust/crates/text/src/layout.rs new file mode 100644 index 00000000..c0e3a198 --- /dev/null +++ b/rust/crates/text/src/layout.rs @@ -0,0 +1,163 @@ +use crate::params::{ + FONT_SIZE_SCALE_REFERENCE, TextAlign, TextBackground, TextDecoration, TextLayoutParams, +}; + +pub const TEXT_DECORATION_THICKNESS_RATIO: f64 = 0.07; +pub const STRIKETHROUGH_VERTICAL_RATIO: f64 = 0.35; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ResolvedTextLayout { + pub scaled_font_size: f64, + pub letter_spacing: f64, + pub line_height_px: f64, + pub font_size_ratio: f64, + pub text_align: TextAlign, + pub text_decoration: TextDecoration, +} + +pub fn resolve_text_layout(text: &TextLayoutParams, canvas_height: f64) -> ResolvedTextLayout { + let scaled_font_size = text.font_size * (canvas_height / FONT_SIZE_SCALE_REFERENCE); + ResolvedTextLayout { + scaled_font_size, + letter_spacing: text.letter_spacing, + line_height_px: scaled_font_size * text.line_height, + font_size_ratio: text.font_size / 15.0, + text_align: text.text_align, + text_decoration: text.text_decoration, + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct LineMetrics { + pub width: f64, + pub ascent: f64, + pub descent: f64, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct TextBlockMeasurement { + pub visual_center_offset: f64, + pub height: f64, + pub max_width: f64, +} + +pub fn measure_text_block( + line_metrics: &[LineMetrics], + line_height_px: f64, +) -> TextBlockMeasurement { + let max_width = line_metrics + .iter() + .fold(0.0_f64, |max, metrics| max.max(metrics.width)); + let line_count = line_metrics.len() as f64; + TextBlockMeasurement { + visual_center_offset: ((line_count - 1.0) * line_height_px) / 2.0, + height: line_count * line_height_px, + max_width, + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Rect { + pub left: f64, + pub top: f64, + pub width: f64, + pub height: f64, +} + +impl Rect { + pub fn right(self) -> f64 { + self.left + self.width + } + + pub fn bottom(self) -> f64 { + self.top + self.height + } +} + +pub fn align_offset(text_align: TextAlign, line_width: f64) -> f64 { + match text_align { + TextAlign::Left => 0.0, + TextAlign::Center => -line_width / 2.0, + TextAlign::Right => -line_width, + } +} + +pub fn text_rect(text_align: TextAlign, block: &TextBlockMeasurement) -> Rect { + Rect { + left: align_offset(text_align, block.max_width), + top: -block.height / 2.0, + width: block.max_width, + height: block.height, + } +} + +fn background_visible(background: &TextBackground) -> bool { + background.enabled && !background.color.is_empty() && background.color != "transparent" +} + +pub fn text_background_rect( + text_align: TextAlign, + block: &TextBlockMeasurement, + background: &TextBackground, + font_size_ratio: f64, +) -> Option<Rect> { + if !background_visible(background) { + return None; + } + let rect = text_rect(text_align, block); + let padding_x = background.padding_x * font_size_ratio; + let padding_y = background.padding_y * font_size_ratio; + Some(Rect { + left: rect.left - padding_x + background.offset_x, + top: rect.top - padding_y + background.offset_y, + width: rect.width + padding_x * 2.0, + height: rect.height + padding_y * 2.0, + }) +} + +pub fn text_visual_rect( + text_align: TextAlign, + block: &TextBlockMeasurement, + background: &TextBackground, + font_size_ratio: f64, +) -> Rect { + let rect = text_rect(text_align, block); + let Some(background_rect) = text_background_rect(text_align, block, background, font_size_ratio) + else { + return rect; + }; + let left = rect.left.min(background_rect.left); + let top = rect.top.min(background_rect.top); + Rect { + left, + top, + width: rect.right().max(background_rect.right()) - left, + height: rect.bottom().max(background_rect.bottom()) - top, + } +} + +pub fn decoration_rect( + text_decoration: TextDecoration, + text_align: TextAlign, + line_width: f64, + line_y: f64, + ascent: f64, + descent: f64, + scaled_font_size: f64, +) -> Option<Rect> { + if text_decoration == TextDecoration::None { + return None; + } + let thickness = (scaled_font_size * TEXT_DECORATION_THICKNESS_RATIO).max(1.0); + let top = match text_decoration { + TextDecoration::Underline => line_y + descent + thickness, + TextDecoration::LineThrough => line_y - (ascent - descent) * STRIKETHROUGH_VERTICAL_RATIO, + TextDecoration::None => unreachable!(), + }; + Some(Rect { + left: align_offset(text_align, line_width), + top, + width: line_width, + height: thickness, + }) +} |
