lumen_text/shaped.rs
1//! D4: the shaped-once main-world store.
2//!
3//! [`ShapedText`] is a per-entity component produced once per change in the
4//! main world (see the runtime/layout producer) and read by the editing
5//! logic ([`crate::TextGeometry`] queries) without any shaper dependency in
6//! `lumen-input`. It also carries the [`crate::ShapedRun`] so the render
7//! world can eventually consume the same shape via the extract (O1) instead
8//! of reshaping; that render-consume step (D4-R) is separable and not yet
9//! wired.
10//!
11//! Placement note: the doc (section 4.2) sketched `ShapedText` in
12//! `lumen-core`, but `lumen-core` must not depend on `lumen-text` (that
13//! would form a cycle: `lumen-text` already depends on `lumen-core`).
14//! The component therefore lives here in `lumen-text`, which owns
15//! [`crate::ShapedRun`] / [`crate::TextGeometry`]; every consumer
16//! (`lumen-input`, the layout producer) already depends on `lumen-text`.
17
18use bevy_ecs::prelude::Component;
19
20use crate::{ShapeOptions, ShapedRun, TextGeometry, TextShaper};
21
22/// One shaped-once result for an entity's text, produced in the main world.
23///
24/// Consumed by both the edit systems (via [`Self::geometry`]) and, once
25/// D4-R lands, the extract (via [`Self::run`]). Invalidated by the
26/// producer when [`Self::shape_version`] changes.
27#[derive(Component, Clone, Debug)]
28pub struct ShapedText {
29 /// Glyphs + fonts, for the render draw path (D4-R consumer).
30 pub run: ShapedRun,
31 /// Byte<->pixel map, for the edit + IME consumers (D4a-e).
32 pub geometry: TextGeometry,
33 /// Version tag the shape was built from: [`crate::TextBuffer`]-style
34 /// content version folded with size / width / wrap / family / weight.
35 /// Lets the producer skip unchanged entities and lets a future
36 /// `ExtractedText` damage diff compare a scalar rather than the glyph
37 /// vec.
38 pub shape_version: u64,
39}
40
41/// Companion viewport metrics written by the same producer alongside
42/// [`ShapedText`]. Keeps [`TextGeometry`] a pure byte<->pixel map: the page
43/// motion router (D7) reads `page_lines = (inner.y / line_h).floor()` from
44/// here. (D7 itself is deferred, its consumer lives in the locked
45/// `lumen-runtime`, but the producer still populates this so the data is
46/// ready.)
47#[derive(Component, Clone, Copy, Debug)]
48pub struct TextViewport {
49 /// Inner content box (box size minus padding), logical px.
50 pub inner: glam::Vec2,
51 /// Resolved line height, logical px (CSS `line-height`, else
52 /// `size_px * DEFAULT_LINE_HEIGHT_MULTIPLIER`).
53 pub line_h: f32,
54}
55
56/// Pure, headless-testable shaping builder (O2): shape `display` once at
57/// `size_px` under `opts`, bake the caret metrics, and package the result.
58///
59/// An empty / unshapeable `display` yields a ShapedText with an empty run
60/// and empty geometry (rather than `None`), so an empty focused input still
61/// carries a stable [`ShapedText`] the caret / IME consumers can read and
62/// the producer can version-cache. The `Option` is retained for API
63/// symmetry; it is presently always `Some`.
64pub fn build_shaped_text(
65 shaper: &mut dyn TextShaper,
66 display: &str,
67 size_px: f32,
68 opts: ShapeOptions,
69 shape_version: u64,
70) -> Option<ShapedText> {
71 // The single fallback-consumption point for this producer: an
72 // already-resolved `opts.line_height` (from `TextStyle::line_height`
73 // via `resolve_line_height`) wins, else `line-height: normal`.
74 let line_height_px = opts
75 .line_height
76 .unwrap_or(size_px * crate::DEFAULT_LINE_HEIGHT_MULTIPLIER);
77 let run = shaper
78 .shape(display, size_px, opts)
79 .unwrap_or_else(|| ShapedRun {
80 font_data: std::sync::Arc::new(Vec::new()),
81 font_index: 0,
82 glyphs: Vec::new(),
83 segments: Vec::new(),
84 width: 0.0,
85 });
86 let geometry = TextGeometry::from(&run).with_size(size_px, line_height_px);
87 Some(ShapedText {
88 run,
89 geometry,
90 shape_version,
91 })
92}