Skip to main content

mittens_engine/engine/ecs/component/
style.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::component::Component;
3
4/// CSS `display` property values.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Display {
7    Block,
8    Inline,
9    InlineBlock,
10    Flex,
11    None,
12}
13
14/// CSS `position` property values.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum Position {
17    #[default]
18    Static,
19    Relative,
20    Absolute,
21    Fixed,
22}
23
24/// CSS `flex-direction` values.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum FlexDirection {
27    #[default]
28    Row,
29    Column,
30    RowReverse,
31    ColumnReverse,
32}
33
34/// CSS `justify-content` values.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum JustifyContent {
37    #[default]
38    FlexStart,
39    FlexEnd,
40    Center,
41    SpaceBetween,
42    SpaceAround,
43    SpaceEvenly,
44}
45
46/// CSS `align-items` values.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum AlignItems {
49    #[default]
50    Stretch,
51    FlexStart,
52    FlexEnd,
53    Center,
54    Baseline,
55}
56
57/// CSS `flex-wrap` values.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59pub enum FlexWrap {
60    #[default]
61    NoWrap,
62    Wrap,
63    WrapReverse,
64}
65
66/// CSS `overflow-wrap` (legacy: `word-wrap`) values.
67///
68/// Cascade note: in CSS this property inherits. Today we read it only on the
69/// immediate styled TC that contains a `TextComponent` — full cascade is a v2
70/// task (would slot in as a layout pre-pass that resolves inherited props
71/// onto each `StyleComponent`).
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum WordWrapMode {
74    /// CSS `overflow-wrap: normal` — only break at whitespace/token
75    /// boundaries; long unbreakable words may overflow the container rather
76    /// than being split mid-word. Maps to `TextComponent::word_wrap = true`.
77    Normal,
78    /// CSS `overflow-wrap: break-word` — break words at arbitrary points if
79    /// needed to keep the line inside `wrap_at`.
80    ///
81    /// NOTE: Currently behaves identically to `BreakAll` (hard character wrap).
82    BreakWord,
83    /// CSS `word-break: break-all` — break words at arbitrary points.
84    /// Maps to `TextComponent::word_wrap = false` (hard column wrap).
85    BreakAll,
86}
87
88/// CSS `text-align` values.
89///
90/// When non-`Auto`, the layout system positions the text-bearing inner
91/// `TransformComponent` inside the content box per this alignment, and
92/// (if `width`/`height` are `Auto`) shrinks the box to fit the measured
93/// text bounds plus padding. `Auto` leaves the inner T's authored
94/// translation alone.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
96pub enum TextAlign {
97    #[default]
98    Auto,
99    Left,
100    Center,
101    Right,
102}
103
104/// Vertical text alignment within a styled box.
105///
106/// This currently applies to the same text-bearing inner transform that
107/// `text_align` controls horizontally.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
109pub enum VerticalAlign {
110    #[default]
111    Auto,
112    Top,
113    Middle,
114    Bottom,
115}
116
117/// CSS `box-sizing` values.
118///
119/// Controls whether `width` / `height` describe the content area
120/// (`ContentBox`, the W3C default) or the outer padding+border box
121/// (`BorderBox`, the modern best-practice default and cat-engine's default).
122///
123/// Under `BorderBox`, padding eats into the content area, so
124/// `width(25%) + width(75%)` siblings fit a parent's content width exactly
125/// even when each has its own padding.
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
127pub enum BoxSizing {
128    ContentBox,
129    #[default]
130    BorderBox,
131}
132
133/// CSS `overflow` values.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
135pub enum Overflow {
136    #[default]
137    Visible,
138    Hidden,
139    Scroll,
140    Auto,
141}
142
143/// A dimension that can be auto, a fixed glyph-unit value, or a percentage.
144///
145/// Fixed values may be in **glyph units** (1.0 = one monospace character cell)
146/// or **world units** (1.0 = one cat-engine world-space unit). Layout converts
147/// world units to glyph units via the nearest [`LayoutComponent`]'s `unit_scale`.
148#[derive(Debug, Clone, Copy, PartialEq, Default)]
149pub enum SizeDimension {
150    #[default]
151    Auto,
152    /// Fixed size in glyph units.
153    GlyphUnits(f32),
154    /// Fixed size in world units. Resolves to glyph units via
155    /// `wu_value / unit_scale`.
156    WorldUnits(f32),
157    /// Percentage of the containing block's dimension (0.0–100.0).
158    Percent(f32),
159}
160
161/// Four-sided spacing. Each side can be a fixed glyph-unit value or a
162/// percentage of the containing block's inline-axis width (CSS semantic:
163/// percentage padding/margin always resolve against the container's width,
164/// even on the vertical sides).
165#[derive(Debug, Clone, Copy, PartialEq, Default)]
166pub struct EdgeInsets {
167    pub top: SizeDimension,
168    pub right: SizeDimension,
169    pub bottom: SizeDimension,
170    pub left: SizeDimension,
171}
172
173impl EdgeInsets {
174    pub const ZERO: Self = Self {
175        top: SizeDimension::GlyphUnits(0.0),
176        right: SizeDimension::GlyphUnits(0.0),
177        bottom: SizeDimension::GlyphUnits(0.0),
178        left: SizeDimension::GlyphUnits(0.0),
179    };
180
181    pub fn all(v: f32) -> Self {
182        let sd = SizeDimension::GlyphUnits(v);
183        Self {
184            top: sd,
185            right: sd,
186            bottom: sd,
187            left: sd,
188        }
189    }
190
191    pub fn all_dim(sd: SizeDimension) -> Self {
192        Self {
193            top: sd,
194            right: sd,
195            bottom: sd,
196            left: sd,
197        }
198    }
199
200    pub fn axes(vertical: f32, horizontal: f32) -> Self {
201        let v = SizeDimension::GlyphUnits(vertical);
202        let h = SizeDimension::GlyphUnits(horizontal);
203        Self {
204            top: v,
205            right: h,
206            bottom: v,
207            left: h,
208        }
209    }
210
211    pub fn axes_dim(vertical: SizeDimension, horizontal: SizeDimension) -> Self {
212        Self {
213            top: vertical,
214            right: horizontal,
215            bottom: vertical,
216            left: horizontal,
217        }
218    }
219
220    /// Resolve all sides to glyph units against the inline-axis container width.
221    /// `container_w_gu` is the width of the containing block in glyph units;
222    /// `unit_scale` is the nearest `LayoutComponent`'s glyph-unit → world-unit
223    /// factor (used to resolve `SizeDimension::WorldUnits`).
224    pub fn resolve(&self, container_w_gu: f32, unit_scale: f32) -> ResolvedInsets {
225        ResolvedInsets {
226            top: resolve_size_inline(self.top, container_w_gu, unit_scale),
227            right: resolve_size_inline(self.right, container_w_gu, unit_scale),
228            bottom: resolve_size_inline(self.bottom, container_w_gu, unit_scale),
229            left: resolve_size_inline(self.left, container_w_gu, unit_scale),
230        }
231    }
232}
233
234/// Edge insets resolved to absolute glyph units.
235#[derive(Debug, Clone, Copy, PartialEq, Default)]
236pub struct ResolvedInsets {
237    pub top: f32,
238    pub right: f32,
239    pub bottom: f32,
240    pub left: f32,
241}
242
243impl ResolvedInsets {
244    pub const ZERO: Self = Self {
245        top: 0.0,
246        right: 0.0,
247        bottom: 0.0,
248        left: 0.0,
249    };
250    pub fn horizontal(&self) -> f32 {
251        self.left + self.right
252    }
253    pub fn vertical(&self) -> f32 {
254        self.top + self.bottom
255    }
256}
257
258/// Resolve a `SizeDimension` to **glyph units** against a known container
259/// length (inline-axis).
260///
261/// `Auto` → 0.0 (caller handles `Auto` specially for width/height).
262/// `WorldUnits(v)` → `v / unit_scale` (caller passes the nearest
263/// `LayoutComponent.unit_scale`; `unit_scale = 0.0` is treated as identity to
264/// avoid division blow-ups when something below tries to resolve before a
265/// layout root is in scope).
266pub fn resolve_size_inline(sd: SizeDimension, container_w_gu: f32, unit_scale: f32) -> f32 {
267    match sd {
268        SizeDimension::GlyphUnits(v) => v,
269        SizeDimension::WorldUnits(v) => {
270            if unit_scale.abs() > f32::EPSILON {
271                v / unit_scale
272            } else {
273                v
274            }
275        }
276        SizeDimension::Percent(p) => container_w_gu * p / 100.0,
277        SizeDimension::Auto => 0.0,
278    }
279}
280
281/// A partial CSS style update — `None` fields are left unchanged.
282///
283/// Used with `IntentValue::UpdateStyle` to patch individual fields on an existing
284/// `StyleComponent` without replacing the whole struct.
285#[derive(Debug, Clone, Default)]
286pub struct StylePatch {
287    pub display: Option<Option<Display>>,
288    pub width: Option<SizeDimension>,
289    pub height: Option<SizeDimension>,
290    pub min_width: Option<Option<f32>>,
291    pub max_width: Option<Option<f32>>,
292    pub min_height: Option<Option<f32>>,
293    pub max_height: Option<Option<f32>>,
294    pub margin: Option<EdgeInsets>,
295    pub padding: Option<EdgeInsets>,
296    pub box_sizing: Option<BoxSizing>,
297    pub flex_direction: Option<FlexDirection>,
298    pub justify_content: Option<JustifyContent>,
299    pub align_items: Option<AlignItems>,
300    pub flex_wrap: Option<FlexWrap>,
301    pub row_gap: Option<f32>,
302    pub column_gap: Option<f32>,
303    pub flex_grow: Option<f32>,
304    pub flex_shrink: Option<f32>,
305    pub flex_basis: Option<SizeDimension>,
306    pub position: Option<Position>,
307    pub top: Option<Option<SizeDimension>>,
308    pub right: Option<Option<SizeDimension>>,
309    pub bottom: Option<Option<SizeDimension>>,
310    pub left: Option<Option<SizeDimension>>,
311    pub line_height: Option<f32>,
312    pub font_size: Option<SizeDimension>,
313    pub vertical_align: Option<VerticalAlign>,
314    pub overflow: Option<Overflow>,
315    pub z_index: Option<Option<i32>>,
316    pub background_color: Option<Option<[f32; 4]>>,
317    pub background_z: Option<Option<f32>>,
318    pub color: Option<Option<[f32; 4]>>,
319    pub word_wrap: Option<Option<WordWrapMode>>,
320    pub word_wrap_tokens: Option<Option<Vec<String>>>,
321}
322
323/// All CSS layout properties for a node, in one struct.
324///
325/// This mirrors the browser's "computed style" record — a single bundle per element rather
326/// than dozens of separate ECS components. Paired with
327/// [`HtmlElementComponent`](crate::engine::ecs::component::HtmlElementComponent) (semantic role)
328/// and [`LayoutComponent`](crate::engine::ecs::component::LayoutComponent) at the subtree root.
329///
330/// All size values are in **glyph units** (1.0 = one monospace character cell).
331///
332/// Style resolution order for any property:
333/// 1. `StyleComponent` value (if not the type's `Default`)
334/// 2. `HtmlElementComponent.element_type` UA-default (e.g. `Div` → `Display::Block`)
335/// 3. Layout system built-in fallback
336#[derive(Debug, Clone)]
337pub struct StyleComponent {
338    // ── Display ──────────────────────────────────────────────────────────
339    /// `None` = inherit from `HtmlElementComponent` UA default.
340    pub display: Option<Display>,
341
342    // ── Sizing ───────────────────────────────────────────────────────────
343    pub width: SizeDimension,
344    pub height: SizeDimension,
345    pub min_width: Option<f32>,
346    pub max_width: Option<f32>,
347    pub min_height: Option<f32>,
348    pub max_height: Option<f32>,
349
350    // ── Box model ────────────────────────────────────────────────────────
351    pub margin: EdgeInsets,
352    pub padding: EdgeInsets,
353    /// `box-sizing`. Default: [`BoxSizing::BorderBox`] (cat-engine default).
354    pub box_sizing: BoxSizing,
355
356    // ── Flex container ───────────────────────────────────────────────────
357    pub flex_direction: FlexDirection,
358    pub justify_content: JustifyContent,
359    pub align_items: AlignItems,
360    pub flex_wrap: FlexWrap,
361    pub row_gap: f32,
362    pub column_gap: f32,
363
364    // ── Flex item ────────────────────────────────────────────────────────
365    pub flex_grow: f32,
366    pub flex_shrink: f32,
367    pub flex_basis: SizeDimension,
368
369    // ── Position ─────────────────────────────────────────────────────────
370    pub position: Position,
371    pub top: Option<SizeDimension>,
372    pub right: Option<SizeDimension>,
373    pub bottom: Option<SizeDimension>,
374    pub left: Option<SizeDimension>,
375
376    // ── Text / typography ────────────────────────────────────────────────
377    /// Line height in glyph units. Default: 1.0.
378    pub line_height: f32,
379    /// Visual glyph scale applied by descendant `TextComponent`s.
380    ///
381    /// Carries its unit: `GlyphUnits(1.0)` = one row of glyphs per glyph unit
382    /// (the layout system's intrinsic measure); `WorldUnits(0.08)` = each row
383    /// of glyphs is 0.08 world units tall (the renderer's glyph quad scale).
384    /// Layout resolves to world units via the nearest `LayoutComponent.unit_scale`
385    /// before stamping the value onto descendant `TextComponent`s.
386    /// `Auto` falls back to the descendant's authored font size.
387    pub font_size: SizeDimension,
388    /// Text alignment within the content box. Default: `Auto` (no positioning).
389    pub text_align: TextAlign,
390    /// Vertical text alignment within the content box.
391    ///
392    /// `Auto` preserves the legacy behavior: if `text_align` is set, text is
393    /// vertically centered; otherwise the authored translation is preserved.
394    pub vertical_align: VerticalAlign,
395
396    // ── Overflow ─────────────────────────────────────────────────────────
397    pub overflow: Overflow,
398
399    // ── Stacking ─────────────────────────────────────────────────────────
400    pub z_index: Option<i32>,
401
402    // ── Background ───────────────────────────────────────────────────────
403    /// RGBA background color. When `Some`, LayoutSystem spawns and manages a
404    /// background quad (covering the padding box) as a child of this item's TC.
405    /// When `None`, no background quad is created (or an existing one is removed).
406    pub background_color: Option<[f32; 4]>,
407    /// Optional override for the `__bg` quad's local Z in the item TC's frame.
408    ///
409    /// `None` (default) means layout derives the background Z from the item's
410    /// resolved stacking layer: `resolved_z - 0.5 * LAYER_DISTANCE`. `Some(z)`
411    /// pins the background to an absolute local Z, overriding the half-step
412    /// rule. See `docs/spec/layout-stacking-z-index.md`.
413    pub background_z: Option<f32>,
414
415    // ── Foreground (text) color ──────────────────────────────────────────
416    /// CSS `color`. Inherited by every descendant glyph via the renderable
417    /// ancestor color walk (`RenderableSystem::inherited_color_for_renderable`).
418    /// When `Some`, layout spawns/maintains a `__text_color` `ColorComponent`
419    /// as an immediate child of this item's TC; when `None`, any existing
420    /// helper is removed. Nested styled TCs with their own `color` override
421    /// naturally because their helper sits closer to the glyph in the walk.
422    pub color: Option<[f32; 4]>,
423
424    // ── Text wrap ────────────────────────────────────────────────────────
425    /// `None` = don't override the descendant `TextComponent`'s authored mode.
426    /// `Some(_)` = write through to the descendant TextComponent during layout.
427    /// Does not yet cascade through nested TC boundaries (v2).
428    pub word_wrap: Option<WordWrapMode>,
429    /// Token strings the wrap algorithm may break after when `word_wrap == BreakWord`.
430    /// `None` = inherit the descendant `TextComponent`'s authored tokens.
431    pub word_wrap_tokens: Option<Vec<String>>,
432
433    component: Option<ComponentId>,
434}
435
436impl Default for StyleComponent {
437    fn default() -> Self {
438        Self {
439            display: None,
440            width: SizeDimension::Auto,
441            height: SizeDimension::Auto,
442            min_width: None,
443            max_width: None,
444            min_height: None,
445            max_height: None,
446            margin: EdgeInsets::ZERO,
447            padding: EdgeInsets::ZERO,
448            box_sizing: BoxSizing::BorderBox,
449            flex_direction: FlexDirection::Row,
450            justify_content: JustifyContent::FlexStart,
451            align_items: AlignItems::Stretch,
452            flex_wrap: FlexWrap::NoWrap,
453            row_gap: 0.0,
454            column_gap: 0.0,
455            flex_grow: 0.0,
456            flex_shrink: 1.0,
457            flex_basis: SizeDimension::Auto,
458            position: Position::Static,
459            top: None,
460            right: None,
461            bottom: None,
462            left: None,
463            line_height: 1.0,
464            font_size: SizeDimension::GlyphUnits(1.0),
465            text_align: TextAlign::Auto,
466            vertical_align: VerticalAlign::Auto,
467            overflow: Overflow::Visible,
468            z_index: None,
469            background_color: None,
470            background_z: None,
471            color: None,
472            word_wrap: None,
473            word_wrap_tokens: None,
474            component: None,
475        }
476    }
477}
478
479impl StyleComponent {
480    pub fn new() -> Self {
481        Self::default()
482    }
483
484    /// Apply a `StylePatch`, updating only fields where the patch has `Some(...)`.
485    pub fn apply_patch(&mut self, patch: StylePatch) {
486        if let Some(v) = patch.display {
487            self.display = v;
488        }
489        if let Some(v) = patch.width {
490            self.width = v;
491        }
492        if let Some(v) = patch.height {
493            self.height = v;
494        }
495        if let Some(v) = patch.min_width {
496            self.min_width = v;
497        }
498        if let Some(v) = patch.max_width {
499            self.max_width = v;
500        }
501        if let Some(v) = patch.min_height {
502            self.min_height = v;
503        }
504        if let Some(v) = patch.max_height {
505            self.max_height = v;
506        }
507        if let Some(v) = patch.margin {
508            self.margin = v;
509        }
510        if let Some(v) = patch.padding {
511            self.padding = v;
512        }
513        if let Some(v) = patch.box_sizing {
514            self.box_sizing = v;
515        }
516        if let Some(v) = patch.flex_direction {
517            self.flex_direction = v;
518        }
519        if let Some(v) = patch.justify_content {
520            self.justify_content = v;
521        }
522        if let Some(v) = patch.align_items {
523            self.align_items = v;
524        }
525        if let Some(v) = patch.flex_wrap {
526            self.flex_wrap = v;
527        }
528        if let Some(v) = patch.row_gap {
529            self.row_gap = v;
530        }
531        if let Some(v) = patch.column_gap {
532            self.column_gap = v;
533        }
534        if let Some(v) = patch.flex_grow {
535            self.flex_grow = v;
536        }
537        if let Some(v) = patch.flex_shrink {
538            self.flex_shrink = v;
539        }
540        if let Some(v) = patch.flex_basis {
541            self.flex_basis = v;
542        }
543        if let Some(v) = patch.position {
544            self.position = v;
545        }
546        if let Some(v) = patch.top {
547            self.top = v;
548        }
549        if let Some(v) = patch.right {
550            self.right = v;
551        }
552        if let Some(v) = patch.bottom {
553            self.bottom = v;
554        }
555        if let Some(v) = patch.left {
556            self.left = v;
557        }
558        if let Some(v) = patch.line_height {
559            self.line_height = v;
560        }
561        if let Some(v) = patch.font_size {
562            self.font_size = v;
563        }
564        if let Some(v) = patch.vertical_align {
565            self.vertical_align = v;
566        }
567        if let Some(v) = patch.overflow {
568            self.overflow = v;
569        }
570        if let Some(v) = patch.z_index {
571            self.z_index = v;
572        }
573        if let Some(v) = patch.background_color {
574            self.background_color = v;
575        }
576        if let Some(v) = patch.background_z {
577            self.background_z = v;
578        }
579        if let Some(v) = patch.color {
580            self.color = v;
581        }
582        if let Some(v) = patch.word_wrap {
583            self.word_wrap = v;
584        }
585        if let Some(v) = patch.word_wrap_tokens {
586            self.word_wrap_tokens = v;
587        }
588    }
589}
590
591impl Component for StyleComponent {
592    fn name(&self) -> &'static str {
593        "style"
594    }
595
596    fn set_id(&mut self, id: ComponentId) {
597        self.component = Some(id);
598    }
599
600    fn as_any(&self) -> &dyn std::any::Any {
601        self
602    }
603    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
604        self
605    }
606
607    fn to_mms_ast(
608        &self,
609        _world: &crate::engine::ecs::World,
610    ) -> crate::scripting::ast::ComponentExpression {
611        use crate::engine::ecs::component::ce_helpers::*;
612        // Style is highly complex (50+ fields); for now we emit an empty
613        // `Style {}` so attach_clone/save produce something parseable.
614        // Full-fidelity emission of style fields is a follow-up.
615        ce("Style")
616    }
617}