Skip to main content

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}