Skip to main content

kozan_core/styling/
builder.rs

1//! Type-safe style API — zero CSS parsing, direct `PropertyDeclaration`.
2//!
3//! Fluent builder with batch flush:
4//! ```ignore
5//! div.style()
6//!     .width(px(200.0))
7//!     .height(pct(100.0))
8//!     .background_color(rgb(0.9, 0.3, 0.2))
9//!     .padding(px(16.0));
10//! // ^ all declarations flushed in ONE write on drop
11//! ```
12
13use style::color::AbsoluteColor;
14use style::properties::PropertyDeclaration;
15use style::values::specified::box_::{Display, PositionProperty};
16use style::values::specified::{
17    Color, FontSize, FontWeight, Margin, NonNegativeLengthPercentage, Opacity, Size,
18};
19
20use crate::dom::document_cell::DocumentCell;
21use crate::id::RawId;
22
23/// Batched style builder — collects declarations, flushes in ONE write on drop.
24///
25/// Like JavaScript's `element.style` but batched: setting N properties
26/// costs one lock acquisition, not N. Chrome does the same — style mutations
27/// are batched within a microtask.
28pub struct StyleAccess {
29    cell: DocumentCell,
30    id: RawId,
31    pending: Vec<PropertyDeclaration>,
32}
33
34impl StyleAccess {
35    pub(crate) fn new(cell: DocumentCell, id: RawId) -> Self {
36        Self {
37            cell,
38            id,
39            pending: Vec::new(),
40        }
41    }
42
43    // ── Display ──
44
45    pub fn display(&mut self, value: Display) -> &mut Self {
46        self.pending.push(PropertyDeclaration::Display(value));
47        self
48    }
49
50    // ── Color ──
51
52    pub fn color(&mut self, value: impl Into<AbsoluteColor>) -> &mut Self {
53        use style::values::specified::color::ColorPropertyValue;
54        self.pending
55            .push(PropertyDeclaration::Color(ColorPropertyValue(
56                Color::from_absolute_color(value.into()),
57            )));
58        self
59    }
60
61    pub fn background_color(&mut self, value: impl Into<AbsoluteColor>) -> &mut Self {
62        self.pending.push(PropertyDeclaration::BackgroundColor(
63            Color::from_absolute_color(value.into()),
64        ));
65        self
66    }
67
68    // ── Dimensions ──
69
70    pub fn width(&mut self, value: impl Into<Size>) -> &mut Self {
71        self.pending.push(PropertyDeclaration::Width(value.into()));
72        self
73    }
74
75    pub fn height(&mut self, value: impl Into<Size>) -> &mut Self {
76        self.pending.push(PropertyDeclaration::Height(value.into()));
77        self
78    }
79
80    pub fn min_width(&mut self, value: impl Into<Size>) -> &mut Self {
81        self.pending
82            .push(PropertyDeclaration::MinWidth(value.into()));
83        self
84    }
85
86    pub fn min_height(&mut self, value: impl Into<Size>) -> &mut Self {
87        self.pending
88            .push(PropertyDeclaration::MinHeight(value.into()));
89        self
90    }
91
92    // ── Margin ──
93
94    pub fn margin_top(&mut self, value: impl Into<Margin>) -> &mut Self {
95        self.pending
96            .push(PropertyDeclaration::MarginTop(value.into()));
97        self
98    }
99
100    pub fn margin_right(&mut self, value: impl Into<Margin>) -> &mut Self {
101        self.pending
102            .push(PropertyDeclaration::MarginRight(value.into()));
103        self
104    }
105
106    pub fn margin_bottom(&mut self, value: impl Into<Margin>) -> &mut Self {
107        self.pending
108            .push(PropertyDeclaration::MarginBottom(value.into()));
109        self
110    }
111
112    pub fn margin_left(&mut self, value: impl Into<Margin>) -> &mut Self {
113        self.pending
114            .push(PropertyDeclaration::MarginLeft(value.into()));
115        self
116    }
117
118    pub fn margin(&mut self, value: impl Into<Margin>) -> &mut Self {
119        let v = value.into();
120        self.pending.push(PropertyDeclaration::MarginTop(v.clone()));
121        self.pending
122            .push(PropertyDeclaration::MarginRight(v.clone()));
123        self.pending
124            .push(PropertyDeclaration::MarginBottom(v.clone()));
125        self.pending.push(PropertyDeclaration::MarginLeft(v));
126        self
127    }
128
129    // ── Padding ──
130
131    pub fn padding_top(&mut self, value: impl Into<NonNegativeLengthPercentage>) -> &mut Self {
132        self.pending
133            .push(PropertyDeclaration::PaddingTop(value.into()));
134        self
135    }
136
137    pub fn padding_right(&mut self, value: impl Into<NonNegativeLengthPercentage>) -> &mut Self {
138        self.pending
139            .push(PropertyDeclaration::PaddingRight(value.into()));
140        self
141    }
142
143    pub fn padding_bottom(&mut self, value: impl Into<NonNegativeLengthPercentage>) -> &mut Self {
144        self.pending
145            .push(PropertyDeclaration::PaddingBottom(value.into()));
146        self
147    }
148
149    pub fn padding_left(&mut self, value: impl Into<NonNegativeLengthPercentage>) -> &mut Self {
150        self.pending
151            .push(PropertyDeclaration::PaddingLeft(value.into()));
152        self
153    }
154
155    pub fn padding(&mut self, value: impl Into<NonNegativeLengthPercentage>) -> &mut Self {
156        let v = value.into();
157        self.pending
158            .push(PropertyDeclaration::PaddingTop(v.clone()));
159        self.pending
160            .push(PropertyDeclaration::PaddingRight(v.clone()));
161        self.pending
162            .push(PropertyDeclaration::PaddingBottom(v.clone()));
163        self.pending.push(PropertyDeclaration::PaddingLeft(v));
164        self
165    }
166
167    // ── Position ──
168
169    pub fn position(&mut self, value: PositionProperty) -> &mut Self {
170        self.pending.push(PropertyDeclaration::Position(value));
171        self
172    }
173
174    // ── Font ──
175
176    pub fn font_size(&mut self, value: FontSize) -> &mut Self {
177        self.pending.push(PropertyDeclaration::FontSize(value));
178        self
179    }
180
181    pub fn font_weight(&mut self, value: FontWeight) -> &mut Self {
182        self.pending.push(PropertyDeclaration::FontWeight(value));
183        self
184    }
185
186    /// Set font-family from a name string (e.g., "Cairo", "Roboto").
187    ///
188    /// Chrome equivalent: setting `element.style.fontFamily`.
189    /// Constructs a Stylo `FontFamily` with a single named family.
190    ///
191    /// For multiple families or generics, use `set_attribute("style", "font-family: ...")`.
192    pub fn font_family(&mut self, name: &str) -> &mut Self {
193        use style::Atom;
194        use style::values::computed::font::{
195            FamilyName, FontFamilyList, FontFamilyNameSyntax, SingleFontFamily,
196        };
197        use style::values::specified::font::FontFamily;
198
199        let list = FontFamilyList {
200            list: style::ArcSlice::from_iter(std::iter::once(SingleFontFamily::FamilyName(
201                FamilyName {
202                    name: Atom::from(name),
203                    syntax: FontFamilyNameSyntax::Quoted,
204                },
205            ))),
206        };
207        self.pending
208            .push(PropertyDeclaration::FontFamily(FontFamily::Values(list)));
209        self
210    }
211
212    // ── Opacity ──
213
214    pub fn opacity(&mut self, value: Opacity) -> &mut Self {
215        self.pending.push(PropertyDeclaration::Opacity(value));
216        self
217    }
218
219    // ── Flex ──
220
221    pub fn flex_grow(&mut self, value: f32) -> &mut Self {
222        use style::values::generics::NonNegative;
223        use style::values::specified::Number;
224        self.pending
225            .push(PropertyDeclaration::FlexGrow(NonNegative(Number::new(
226                value,
227            ))));
228        self
229    }
230
231    pub fn flex_shrink(&mut self, value: f32) -> &mut Self {
232        use style::values::generics::NonNegative;
233        use style::values::specified::Number;
234        self.pending
235            .push(PropertyDeclaration::FlexShrink(NonNegative(Number::new(
236                value,
237            ))));
238        self
239    }
240
241    pub fn flex_basis(&mut self, value: impl Into<Size>) -> &mut Self {
242        use style::values::generics::flex::GenericFlexBasis;
243        use style::values::generics::length::GenericSize;
244        let size = value.into();
245        let basis = match size {
246            GenericSize::Auto => GenericFlexBasis::Content,
247            other => GenericFlexBasis::Size(other),
248        };
249        self.pending
250            .push(PropertyDeclaration::FlexBasis(Box::new(basis)));
251        self
252    }
253
254    // ── Gap ──
255
256    pub fn gap(&mut self, value: impl Into<NonNegativeLengthPercentage>) -> &mut Self {
257        use style::values::generics::length::GenericLengthPercentageOrNormal;
258        let v: NonNegativeLengthPercentage = value.into();
259        self.pending.push(PropertyDeclaration::RowGap(
260            GenericLengthPercentageOrNormal::LengthPercentage(v.clone()),
261        ));
262        self.pending.push(PropertyDeclaration::ColumnGap(
263            GenericLengthPercentageOrNormal::LengthPercentage(v),
264        ));
265        self
266    }
267
268    // ── Flex layout ──
269
270    pub fn flex_direction(
271        &mut self,
272        value: style::computed_values::flex_direction::T,
273    ) -> &mut Self {
274        self.pending.push(PropertyDeclaration::FlexDirection(value));
275        self
276    }
277
278    pub fn flex_wrap(&mut self, value: style::computed_values::flex_wrap::T) -> &mut Self {
279        self.pending.push(PropertyDeclaration::FlexWrap(value));
280        self
281    }
282
283    /// `display: flex; flex-direction: row`
284    pub fn flex_row(&mut self) -> &mut Self {
285        self.display(Display::Flex);
286        self.flex_direction(style::computed_values::flex_direction::T::Row)
287    }
288
289    /// `display: flex; flex-direction: column`
290    pub fn flex_col(&mut self) -> &mut Self {
291        self.display(Display::Flex);
292        self.flex_direction(style::computed_values::flex_direction::T::Column)
293    }
294
295    // ── Alignment ──
296
297    pub fn align_items_center(&mut self) -> &mut Self {
298        use style::values::specified::align::{AlignFlags, ItemPlacement};
299        self.pending
300            .push(PropertyDeclaration::AlignItems(ItemPlacement(
301                AlignFlags::CENTER,
302            )));
303        self
304    }
305    pub fn align_items_start(&mut self) -> &mut Self {
306        use style::values::specified::align::{AlignFlags, ItemPlacement};
307        self.pending
308            .push(PropertyDeclaration::AlignItems(ItemPlacement(
309                AlignFlags::FLEX_START,
310            )));
311        self
312    }
313    pub fn align_items_end(&mut self) -> &mut Self {
314        use style::values::specified::align::{AlignFlags, ItemPlacement};
315        self.pending
316            .push(PropertyDeclaration::AlignItems(ItemPlacement(
317                AlignFlags::FLEX_END,
318            )));
319        self
320    }
321    pub fn align_items_stretch(&mut self) -> &mut Self {
322        use style::values::specified::align::{AlignFlags, ItemPlacement};
323        self.pending
324            .push(PropertyDeclaration::AlignItems(ItemPlacement(
325                AlignFlags::STRETCH,
326            )));
327        self
328    }
329
330    pub fn justify_content_center(&mut self) -> &mut Self {
331        use style::values::specified::align::{AlignFlags, ContentDistribution};
332        self.pending.push(PropertyDeclaration::JustifyContent(
333            ContentDistribution::new(AlignFlags::CENTER),
334        ));
335        self
336    }
337    pub fn justify_content_start(&mut self) -> &mut Self {
338        use style::values::specified::align::{AlignFlags, ContentDistribution};
339        self.pending.push(PropertyDeclaration::JustifyContent(
340            ContentDistribution::new(AlignFlags::FLEX_START),
341        ));
342        self
343    }
344    pub fn justify_content_end(&mut self) -> &mut Self {
345        use style::values::specified::align::{AlignFlags, ContentDistribution};
346        self.pending.push(PropertyDeclaration::JustifyContent(
347            ContentDistribution::new(AlignFlags::FLEX_END),
348        ));
349        self
350    }
351    pub fn justify_content_between(&mut self) -> &mut Self {
352        use style::values::specified::align::{AlignFlags, ContentDistribution};
353        self.pending.push(PropertyDeclaration::JustifyContent(
354            ContentDistribution::new(AlignFlags::SPACE_BETWEEN),
355        ));
356        self
357    }
358
359    // ── Border ──
360
361    pub fn border_radius(
362        &mut self,
363        v: impl Into<NonNegativeLengthPercentage> + Clone,
364    ) -> &mut Self {
365        use style::values::generics::border::GenericBorderCornerRadius;
366        use style::values::generics::size::Size2D;
367        let lp = v.into();
368        let r = GenericBorderCornerRadius(Size2D::new(lp.clone(), lp));
369        self.pending
370            .push(PropertyDeclaration::BorderTopLeftRadius(Box::new(
371                r.clone(),
372            )));
373        self.pending
374            .push(PropertyDeclaration::BorderTopRightRadius(Box::new(
375                r.clone(),
376            )));
377        self.pending
378            .push(PropertyDeclaration::BorderBottomRightRadius(Box::new(
379                r.clone(),
380            )));
381        self.pending
382            .push(PropertyDeclaration::BorderBottomLeftRadius(Box::new(r)));
383        self
384    }
385
386    pub fn border_width(&mut self, v: impl Into<NonNegativeLengthPercentage> + Clone) -> &mut Self {
387        // border-width uses NonNegativeLength (from Au), but we can use the medium keyword
388        // fallback. For exact px, use set_attribute("style", "border-width: Npx").
389        // Here we use a CSS string approach internally.
390        let lp = v.into();
391        // Extract px value from the NonNegativeLengthPercentage
392        self.pending.push(PropertyDeclaration::BorderTopWidth(
393            style::values::specified::BorderSideWidth::from_px(self.extract_px(&lp)),
394        ));
395        self.pending.push(PropertyDeclaration::BorderRightWidth(
396            style::values::specified::BorderSideWidth::from_px(self.extract_px(&lp)),
397        ));
398        self.pending.push(PropertyDeclaration::BorderBottomWidth(
399            style::values::specified::BorderSideWidth::from_px(self.extract_px(&lp)),
400        ));
401        self.pending.push(PropertyDeclaration::BorderLeftWidth(
402            style::values::specified::BorderSideWidth::from_px(self.extract_px(&lp)),
403        ));
404        self
405    }
406
407    pub fn border_style(&mut self, v: style::values::specified::border::BorderStyle) -> &mut Self {
408        self.pending.push(PropertyDeclaration::BorderTopStyle(v));
409        self.pending.push(PropertyDeclaration::BorderRightStyle(v));
410        self.pending.push(PropertyDeclaration::BorderBottomStyle(v));
411        self.pending.push(PropertyDeclaration::BorderLeftStyle(v));
412        self
413    }
414
415    pub fn border_color(&mut self, v: impl Into<AbsoluteColor>) -> &mut Self {
416        let c = Color::from_absolute_color(v.into());
417        self.pending
418            .push(PropertyDeclaration::BorderTopColor(c.clone()));
419        self.pending
420            .push(PropertyDeclaration::BorderRightColor(c.clone()));
421        self.pending
422            .push(PropertyDeclaration::BorderBottomColor(c.clone()));
423        self.pending.push(PropertyDeclaration::BorderLeftColor(c));
424        self
425    }
426
427    pub fn border_bottom_width_px(&mut self, v: f32) -> &mut Self {
428        self.pending.push(PropertyDeclaration::BorderBottomWidth(
429            style::values::specified::BorderSideWidth::from_px(v),
430        ));
431        self
432    }
433
434    pub fn border_bottom_style(
435        &mut self,
436        v: style::values::specified::border::BorderStyle,
437    ) -> &mut Self {
438        self.pending.push(PropertyDeclaration::BorderBottomStyle(v));
439        self
440    }
441
442    pub fn border_bottom_color(&mut self, v: impl Into<AbsoluteColor>) -> &mut Self {
443        self.pending.push(PropertyDeclaration::BorderBottomColor(
444            Color::from_absolute_color(v.into()),
445        ));
446        self
447    }
448
449    pub fn border_right_width_px(&mut self, v: f32) -> &mut Self {
450        self.pending.push(PropertyDeclaration::BorderRightWidth(
451            style::values::specified::BorderSideWidth::from_px(v),
452        ));
453        self
454    }
455
456    pub fn border_right_style(
457        &mut self,
458        v: style::values::specified::border::BorderStyle,
459    ) -> &mut Self {
460        self.pending.push(PropertyDeclaration::BorderRightStyle(v));
461        self
462    }
463
464    pub fn border_right_color(&mut self, v: impl Into<AbsoluteColor>) -> &mut Self {
465        self.pending.push(PropertyDeclaration::BorderRightColor(
466            Color::from_absolute_color(v.into()),
467        ));
468        self
469    }
470
471    // ── Overflow ──
472
473    pub fn overflow_hidden(&mut self) -> &mut Self {
474        use style::values::specified::box_::Overflow;
475        self.pending
476            .push(PropertyDeclaration::OverflowX(Overflow::Hidden));
477        self.pending
478            .push(PropertyDeclaration::OverflowY(Overflow::Hidden));
479        self
480    }
481
482    // ── Internal helper ──
483
484    #[inline]
485    fn extract_px(&self, lp: &NonNegativeLengthPercentage) -> f32 {
486        use style::values::specified::length::NoCalcLength;
487        match &lp.0 {
488            style::values::specified::LengthPercentage::Length(NoCalcLength::Absolute(
489                style::values::specified::length::AbsoluteLength::Px(v),
490            )) => *v,
491            _ => 0.0,
492        }
493    }
494
495    // ── Short aliases (GPUI-style) ──
496    // One letter or short name, same batched flush.
497    //
498    // container.style().w(px(200.0)).h(px(100.0)).bg(rgb8(232, 76, 61));
499
500    /// Short for `width`.
501    pub fn w(&mut self, v: impl Into<Size>) -> &mut Self {
502        self.width(v)
503    }
504    /// Short for `height`.
505    pub fn h(&mut self, v: impl Into<Size>) -> &mut Self {
506        self.height(v)
507    }
508    /// Width + height (square).
509    pub fn size(&mut self, v: impl Into<Size> + Clone) -> &mut Self {
510        self.width(v.clone());
511        self.height(v)
512    }
513    /// Short for `background_color`.
514    pub fn bg(&mut self, v: impl Into<AbsoluteColor>) -> &mut Self {
515        self.background_color(v)
516    }
517    /// `display: flex`.
518    pub fn flex(&mut self) -> &mut Self {
519        self.display(Display::Flex)
520    }
521    /// `display: grid`.
522    pub fn grid(&mut self) -> &mut Self {
523        self.display(Display::Grid)
524    }
525    /// `display: block`.
526    pub fn block(&mut self) -> &mut Self {
527        self.display(Display::Block)
528    }
529    /// Short for `padding` (all 4).
530    pub fn pad(&mut self, v: impl Into<NonNegativeLengthPercentage>) -> &mut Self {
531        self.padding(v)
532    }
533    /// Short for `margin` (all 4).
534    pub fn mar(&mut self, v: impl Into<Margin>) -> &mut Self {
535        self.margin(v)
536    }
537
538    // ── Raw (advanced) ──
539
540    pub fn raw(&mut self, decl: PropertyDeclaration) -> &mut Self {
541        self.pending.push(decl);
542        self
543    }
544
545    // ── Explicit flush ──
546
547    /// Flush all pending declarations NOW.
548    /// Not needed for chains — Drop flushes automatically at semicolon.
549    pub fn apply(&mut self) {
550        self.flush();
551    }
552
553    fn flush(&mut self) {
554        if self.pending.is_empty() {
555            return;
556        }
557        let pending = std::mem::take(&mut self.pending);
558        let index = self.id.index();
559        self.cell.write(|doc| {
560            doc.write_element_data(self.id, |ed| {
561                for decl in pending {
562                    ed.set_inline_property(decl);
563                }
564            });
565            // Mark element + propagate dirty_descendants up ancestors.
566            doc.mark_for_restyle(index);
567        });
568    }
569}
570
571/// Auto-flush on drop — chains flush at the semicolon.
572impl Drop for StyleAccess {
573    fn drop(&mut self) {
574        self.flush();
575    }
576}