1
//! Microsoft Office-style ribbon widget.
2
//!
3
//! Models the component hierarchy of the MS Ribbon (Office "Fluent" ribbon /
4
//! RibbonX `customUI` markup / Windows Ribbon Framework):
5
//!
6
//! ```text
7
//! Ribbon ─ app button ("FILE")            RibbonAppButton
8
//!        ─ tabs                           RibbonTab
9
//!            └─ groups                    RibbonGroup (label + dialog launcher)
10
//!                 └─ items                RibbonItem
11
//!                      ├─ LargeButton     RibbonButton (icon-over-label, full height)
12
//!                      ├─ SmallButton     RibbonButton (16px icon row)
13
//!                      ├─ Column / Row    RibbonColumn / RibbonRow (packing boxes)
14
//!                      ├─ Combo           embeds [`super::combobox::ComboBox`]
15
//!                      ├─ Drop            embeds [`super::drop_down::DropDown`]
16
//!                      ├─ Check           embeds [`super::check_box::CheckBox`]
17
//!                      ├─ Gallery         RibbonGallery (in-ribbon gallery + spinner)
18
//!                      ├─ Separator       thin vertical rule
19
//!                      └─ Custom          any user [`Dom`]
20
//! ```
21
//!
22
//! Mapping from RibbonX elements: `button[size=large]` → `LargeButton`,
23
//! `button`/`toggleButton` → `SmallButton` (+ `toggled`), `splitButton`/`menu`
24
//! → `RibbonArrow::Split`/`Menu`, `box`/`buttonGroup` → `Row`/`Column`,
25
//! `comboBox` → `Combo`, `dropDown` → `Drop`, `checkBox` → `Check`,
26
//! `gallery` → `Gallery`, `separator` → `Separator`,
27
//! `dialogBoxLauncher` → [`RibbonGroup::launcher`]. Contextual tabs, KeyTips,
28
//! the backstage view and automatic size collapsing are out of scope.
29
//!
30
//! Buttons are not re-implemented: every ribbon button (including the group
31
//! dialog launcher and the gallery spinner buttons) expands to the existing
32
//! [`super::button::Button`] widget with ribbon part styles injected through
33
//! `Button`'s public style fields. Embedded `Combo`/`Drop`/`Check` widgets
34
//! render exactly as configured — restyle them via their own public
35
//! `*_style` fields (see the ribbon example for an office-2013-style combobox).
36
//!
37
//! All visual parts of the ribbon itself are exposed on [`RibbonStyle`]
38
//! (defaults = the Office-2013-era look look, [`RibbonStyle::office_2013`]); replace any field
39
//! to re-theme without touching widget code.
40

            
41
use azul_core::{
42
    callbacks::{CoreCallback, CoreCallbackData, Update},
43
    dom::{
44
        Dom, DomNodeId, DomVec, EventFilter, HoverEventFilter, IdOrClass, IdOrClass::Class,
45
        IdOrClassVec,
46
    },
47
    refany::RefAny,
48
};
49
#[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
50
use azul_css::{
51
    dynamic_selector::{
52
        CssPropertyWithConditions as Cond, CssPropertyWithConditionsVec, DynamicSelector,
53
        MinMaxRange,
54
    },
55
    props::{
56
        basic::{color::ColorU, font::{StyleFontFamily, StyleFontFamilyVec}, *},
57
        layout::*,
58
        property::CssProperty as P,
59
        style::*,
60
    },
61
    *,
62
};
63

            
64
use azul_css::{impl_option, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_mut};
65
use azul_css::system::{Handedness, SystemStyle};
66

            
67
use crate::callbacks::{Callback, CallbackInfo};
68

            
69
use super::{
70
    button::{Button, OptionButtonOnClick},
71
    check_box::CheckBox,
72
    combobox::ComboBox,
73
    drop_down::DropDown,
74
};
75

            
76
// -- Callbacks --
77

            
78
/// Callback signature invoked when a ribbon tab is clicked.
79
pub type RibbonOnTabClickCallbackType = extern "C" fn(RefAny, CallbackInfo, usize) -> Update;
80
impl_widget_callback!(
81
    RibbonOnTabClick, OptionRibbonOnTabClick,
82
    RibbonOnTabClickCallback, RibbonOnTabClickCallbackType
83
);
84

            
85
azul_core::impl_managed_callback! {
86
    wrapper:        RibbonOnTabClickCallback,
87
    info_ty:        CallbackInfo,
88
    return_ty:      Update,
89
    default_ret:    Update::DoNothing,
90
    invoker_static: RIBBON_ON_TAB_CLICK_INVOKER,
91
    invoker_ty:     AzRibbonOnTabClickCallbackInvoker,
92
    thunk_fn:       az_ribbon_on_tab_click_callback_thunk,
93
    setter_fn:      AzApp_setRibbonOnTabClickCallbackInvoker,
94
    from_handle_fn: AzRibbonOnTabClickCallback_createFromHostHandle,
95
    extra_args:     [ tab_index: usize ],
96
}
97

            
98
/// Callback signature invoked when a gallery cell is clicked (cell index).
99
pub type RibbonGalleryOnSelectCallbackType =
100
    extern "C" fn(RefAny, CallbackInfo, usize) -> Update;
101
impl_widget_callback!(
102
    RibbonGalleryOnSelect, OptionRibbonGalleryOnSelect,
103
    RibbonGalleryOnSelectCallback, RibbonGalleryOnSelectCallbackType
104
);
105

            
106
azul_core::impl_managed_callback! {
107
    wrapper:        RibbonGalleryOnSelectCallback,
108
    info_ty:        CallbackInfo,
109
    return_ty:      Update,
110
    default_ret:    Update::DoNothing,
111
    invoker_static: RIBBON_GALLERY_ON_SELECT_INVOKER,
112
    invoker_ty:     AzRibbonGalleryOnSelectCallbackInvoker,
113
    thunk_fn:       az_ribbon_gallery_on_select_callback_thunk,
114
    setter_fn:      AzApp_setRibbonGalleryOnSelectCallbackInvoker,
115
    from_handle_fn: AzRibbonGalleryOnSelectCallback_createFromHostHandle,
116
    extra_args:     [ cell_index: usize ],
117
}
118

            
119
// -- Font --
120

            
121
const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
122
const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
123
const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
124
    StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
125

            
126
// -- the Office-2013-era look palette (seeds RibbonTheme::office_2013) --
127

            
128
const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
129
const TRANSPARENT: ColorU = ColorU { r: 0, g: 0, b: 0, a: 0 };
130
/// Office 2013 accent blue (#2B579A): FILE tab, active tab text.
131
const W13_BLUE: ColorU = ColorU { r: 43, g: 87, b: 154, a: 255 };
132
/// FILE tab hover fill (darker blue).
133
const W13_BLUE_HOVER: ColorU = ColorU { r: 30, g: 62, b: 111, a: 255 };
134
/// Regular control text (#444444).
135
const W13_TEXT: ColorU = ColorU { r: 68, g: 68, b: 68, a: 255 };
136
/// Group caption + secondary glyph gray (#676767).
137
const W13_LABEL_GRAY: ColorU = ColorU { r: 103, g: 103, b: 103, a: 255 };
138
/// Monochrome icon gray.
139
const W13_ICON_GRAY: ColorU = ColorU { r: 80, g: 80, b: 80, a: 255 };
140
/// Chrome border gray (#D4D4D4): tab underline, ribbon bottom border.
141
const W13_BORDER: ColorU = ColorU { r: 212, g: 212, b: 212, a: 255 };
142
/// Group/segment separator gray (#E1E1E1).
143
const W13_SEP: ColorU = ColorU { r: 225, g: 225, b: 225, a: 255 };
144
/// Hover fill (#CDE6F7).
145
const W13_HOVER_BG: ColorU = ColorU { r: 205, g: 230, b: 247, a: 255 };
146
/// Hover/checked border (#92C0E0).
147
const W13_HOVER_BORDER: ColorU = ColorU { r: 146, g: 192, b: 224, a: 255 };
148
/// Pressed fill (#B0D0EC).
149
const W13_PRESSED_BG: ColorU = ColorU { r: 176, g: 208, b: 236, a: 255 };
150
/// Toggled-on fill (#C6DDF0).
151
const W13_CHECKED_BG: ColorU = ColorU { r: 198, g: 221, b: 240, a: 255 };
152
/// Selected gallery cell fill (#EAF3FC).
153
const W13_SELECTED_BG: ColorU = ColorU { r: 234, g: 243, b: 252, a: 255 };
154
/// Flat editable-field border gray (#ABABAB).
155
const W13_FIELD_BORDER: ColorU = ColorU { r: 171, g: 171, b: 171, a: 255 };
156

            
157
// -- Theme --
158

            
159
/// Color palette from which a full [`RibbonStyle`] is derived via
160
/// [`RibbonStyle::from_theme`]. All fields are plain colors, so themes are
161
/// trivially constructible over FFI. Presets: [`RibbonTheme::office_2013`]
162
/// (the default) and [`RibbonTheme::from_system`], which extracts the
163
/// colors from the OS theme (accent color, selection color, separators).
164
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
165
#[repr(C)]
166
pub struct RibbonTheme {
167
    /// Chrome background: tab bar and ribbon content.
168
    pub chrome_bg: ColorU,
169
    /// Accent: application button fill, active tab text.
170
    pub accent: ColorU,
171
    /// Application button hover fill.
172
    pub accent_hover: ColorU,
173
    /// Text on accent fills (application button label).
174
    pub accent_text: ColorU,
175
    /// Regular control text.
176
    pub text: ColorU,
177
    /// Group captions and secondary glyphs.
178
    pub label: ColorU,
179
    /// Monochrome icon glyphs.
180
    pub icon: ColorU,
181
    /// Chrome borders: tab underline, ribbon bottom border, gallery frame.
182
    pub border: ColorU,
183
    /// Group and segment separators.
184
    pub separator: ColorU,
185
    /// Hover fill on ribbon controls.
186
    pub hover_bg: ColorU,
187
    /// Hover and toggled-on border.
188
    pub hover_border: ColorU,
189
    /// Pressed fill.
190
    pub pressed_bg: ColorU,
191
    /// Toggled-on fill.
192
    pub checked_bg: ColorU,
193
    /// Selected gallery cell fill.
194
    pub selected_bg: ColorU,
195
    /// Editable field border (embedded comboboxes).
196
    pub field_border: ColorU,
197
}
198

            
199
impl RibbonTheme {
200
    /// The the Office-2013-era look palette: white chrome, #2B579A accents, #CDE6F7 hovers.
201
    #[must_use]
202
1240
    pub const fn office_2013() -> Self {
203
1240
        Self {
204
1240
            chrome_bg: WHITE,
205
1240
            accent: W13_BLUE,
206
1240
            accent_hover: W13_BLUE_HOVER,
207
1240
            accent_text: WHITE,
208
1240
            text: W13_TEXT,
209
1240
            label: W13_LABEL_GRAY,
210
1240
            icon: W13_ICON_GRAY,
211
1240
            border: W13_BORDER,
212
1240
            separator: W13_SEP,
213
1240
            hover_bg: W13_HOVER_BG,
214
1240
            hover_border: W13_HOVER_BORDER,
215
1240
            pressed_bg: W13_PRESSED_BG,
216
1240
            checked_bg: W13_CHECKED_BG,
217
1240
            selected_bg: W13_SELECTED_BG,
218
1240
            field_border: W13_FIELD_BORDER,
219
1240
        }
220
1240
    }
221

            
222
    /// Extracts a ribbon palette from the OS theme (accent color, selection
223
    /// colors, separators). Colors the platform does not report fall back to
224
    /// the the Office-2013-era look palette. Pass `SystemStyle::detect()` for the live
225
    /// system theme, or a preset `SystemStyle` for platform mockups.
226
    /// Takes the style by value (FFI constructor convention).
227
    #[must_use]
228
5
    pub fn from_system(style: SystemStyle) -> Self {
229
5
        let d = Self::office_2013();
230
5
        let c = &style.colors;
231
        // Each ribbon field maps to ONE system color; a color the platform
232
        // does not report falls back to that field's own the Office-2013-era look value
233
        // (never to another derived value). No color arithmetic on purpose:
234
        // FFI-observable behavior stays trivial to reason about.
235
5
        let accent = c.accent.into_option();
236
5
        let selection = c.selection_background.into_option();
237
5
        let separator = c.separator.into_option();
238
5
        let inactive_selection = c.selection_background_inactive.into_option();
239
5
        let secondary_text = c.secondary_text.into_option();
240
5
        Self {
241
5
            chrome_bg: c.window_background.into_option().unwrap_or(d.chrome_bg),
242
5
            accent: accent.unwrap_or(d.accent),
243
5
            accent_hover: selection.unwrap_or(d.accent_hover),
244
5
            accent_text: c.accent_text.into_option().unwrap_or(d.accent_text),
245
5
            text: c.text.into_option().unwrap_or(d.text),
246
5
            label: secondary_text.unwrap_or(d.label),
247
5
            icon: secondary_text.unwrap_or(d.icon),
248
5
            border: separator.unwrap_or(d.border),
249
5
            separator: separator.unwrap_or(d.separator),
250
5
            hover_bg: inactive_selection.unwrap_or(d.hover_bg),
251
5
            hover_border: accent.unwrap_or(d.hover_border),
252
5
            pressed_bg: selection.unwrap_or(d.pressed_bg),
253
5
            checked_bg: selection.unwrap_or(d.checked_bg),
254
5
            selected_bg: inactive_selection.unwrap_or(d.selected_bg),
255
5
            field_border: separator.unwrap_or(d.field_border),
256
5
        }
257
5
    }
258
}
259

            
260
impl Default for RibbonTheme {
261
    fn default() -> Self {
262
        Self::office_2013()
263
    }
264
}
265

            
266
// -- Colorless const part styles (shared by every theme) --
267

            
268
static GROUP_ITEMS_STYLE: &[Cond] = &[
269
    Cond::simple(P::const_box_sizing(LayoutBoxSizing::BorderBox)),
270
    Cond::simple(P::const_display(LayoutDisplay::Flex)),
271
    Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
272
    Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
273
    Cond::simple(P::const_height(LayoutHeight::const_px(68))),
274
    Cond::simple(P::const_align_items(LayoutAlignItems::Start)),
275
    Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
276
];
277

            
278
static GROUP_FOOTER_STYLE: &[Cond] = &[
279
    Cond::simple(P::const_box_sizing(LayoutBoxSizing::BorderBox)),
280
    Cond::simple(P::const_display(LayoutDisplay::Flex)),
281
    Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
282
    Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
283
    Cond::simple(P::const_height(LayoutHeight::const_px(18))),
284
    Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
285
    Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
286
];
287

            
288
static FOOTER_SPACER_STYLE: &[Cond] = &[
289
    Cond::simple(P::const_width(LayoutWidth::const_px(18))),
290
    Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
291
    Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
292
];
293

            
294
static COLUMN_STYLE: &[Cond] = &[
295
    Cond::simple(P::const_display(LayoutDisplay::Flex)),
296
    Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
297
    Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
298
    Cond::simple(P::const_align_items(LayoutAlignItems::Start)),
299
    Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
300
];
301

            
302
static ROW_STYLE: &[Cond] = &[
303
    Cond::simple(P::const_display(LayoutDisplay::Flex)),
304
    Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
305
    Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
306
    Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
307
    Cond::simple(P::const_margin_bottom(LayoutMarginBottom::const_px(5))),
308
    Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
309
];
310

            
311
static GALLERY_STRIP_STYLE: &[Cond] = &[
312
    Cond::simple(P::const_display(LayoutDisplay::Flex)),
313
    Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
314
    Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
315
    Cond::simple(P::const_overflow_x(LayoutOverflow::Hidden)),
316
    Cond::simple(P::const_overflow_y(LayoutOverflow::Hidden)),
317
];
318

            
319
static RIBBON_COMBO_TEXT_STYLE: &[Cond] = &[
320
    Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
321
    Cond::simple(P::const_text_align(StyleTextAlign::Left)),
322
    Cond::simple(P::const_padding_right(LayoutPaddingRight::const_px(2))),
323
];
324

            
325
// -- Responsive (@media) conditions --
326
//
327
// The mobile ribbon keeps the SEMANTICS of the desktop one - the same tabs,
328
// groups and items - and changes only presentation, so both chromes are
329
// emitted once and the viewport decides which is visible. That is how a
330
// responsive HTML page behaves, and it means no second widget tree, no
331
// duplicated callbacks and no state to keep in sync.
332
//
333
// `MOBILE_MAX_PX` is the breakpoint: at or below it the touch chrome shows.
334

            
335
/// Widest viewport that still gets the touch layout (a large phone in
336
/// landscape is still a phone).
337
pub const MOBILE_MAX_PX: f32 = 720.0;
338

            
339
static COND_MOBILE: &[DynamicSelector] = &[DynamicSelector::ViewportWidth(MinMaxRange {
340
    min: f32::NAN,
341
    max: MOBILE_MAX_PX,
342
})];
343

            
344
static COND_DESKTOP: &[DynamicSelector] = &[DynamicSelector::ViewportWidth(MinMaxRange {
345
    min: MOBILE_MAX_PX,
346
    max: f32::NAN,
347
})];
348

            
349
/// `display: none` unless the viewport is a phone.
350
3708
fn only_on_mobile(prop: P) -> Cond {
351
3708
    Cond::with_single_condition(prop, COND_MOBILE)
352
3708
}
353

            
354
/// `display: none` unless the viewport is a desktop.
355
fn only_on_desktop(prop: P) -> Cond {
356
    Cond::with_single_condition(prop, COND_DESKTOP)
357
}
358

            
359
/// Hidden by default, shown on phones.
360
2472
fn mobile_only_visibility(display: LayoutDisplay) -> [Cond; 2] {
361
2472
    [
362
2472
        Cond::simple(P::const_display(LayoutDisplay::None)),
363
2472
        only_on_mobile(P::const_display(display)),
364
2472
    ]
365
2472
}
366

            
367
/// Visible by default, hidden on phones.
368
fn desktop_only_visibility(display: LayoutDisplay) -> [Cond; 2] {
369
    [
370
        Cond::simple(P::const_display(display)),
371
        only_on_mobile(P::const_display(LayoutDisplay::None)),
372
    ]
373
}
374

            
375
// -- Theme -> property-list builders --
376
//
377
// Every themed ribbon part is built from `RibbonTheme` colors by the
378
// functions below; `RibbonStyle::office_2013()` is just
379
// `from_theme(&RibbonTheme::office_2013())`, so there is exactly one source
380
// of truth for each part's property list.
381

            
382
35846
fn bg_vec(c: ColorU) -> StyleBackgroundContentVec {
383
35846
    StyleBackgroundContentVec::from_vec(vec![StyleBackgroundContent::Color(c)])
384
35846
}
385

            
386
23486
fn cond_bg(c: ColorU) -> Cond {
387
23486
    Cond::simple(P::const_background_content(bg_vec(c)))
388
23486
}
389

            
390
9888
fn cond_bg_hover(c: ColorU) -> Cond {
391
9888
    Cond::on_hover(P::const_background_content(bg_vec(c)))
392
9888
}
393

            
394
2472
fn cond_bg_active(c: ColorU) -> Cond {
395
2472
    Cond::on_active(P::const_background_content(bg_vec(c)))
396
2472
}
397

            
398
21016
const fn cond_text_color(c: ColorU) -> Cond {
399
21016
    Cond::simple(P::const_text_color(StyleTextColor { inner: c }))
400
21016
}
401

            
402
/// the classic office-suite control metrics (22px small button, 66px large button, 26px tab)
403
/// are BORDER-BOX numbers: they include the padding and the 1px hover
404
/// border. CSS defaults to content-box, which inflated every control by its
405
/// padding+border - three 22px rows became 78px and overflowed the 68px item
406
/// area, painting over the group caption.
407
19779
const fn cond_border_box() -> Cond {
408
19779
    Cond::simple(P::const_box_sizing(LayoutBoxSizing::BorderBox))
409
19779
}
410

            
411
13598
fn push_padding(v: &mut Vec<Cond>, top: isize, right: isize, bottom: isize, left: isize) {
412
13598
    v.push(Cond::simple(P::const_padding_top(LayoutPaddingTop::const_px(top))));
413
13598
    v.push(Cond::simple(P::const_padding_right(LayoutPaddingRight::const_px(right))));
414
13598
    v.push(Cond::simple(P::const_padding_bottom(LayoutPaddingBottom::const_px(bottom))));
415
13598
    v.push(Cond::simple(P::const_padding_left(LayoutPaddingLeft::const_px(left))));
416
13598
}
417

            
418
/// 1px solid border on all four sides in the given color.
419
9890
fn push_box_border(v: &mut Vec<Cond>, c: ColorU) {
420
9890
    v.push(Cond::simple(P::const_border_top_width(LayoutBorderTopWidth::const_px(1))));
421
9890
    v.push(Cond::simple(P::const_border_left_width(LayoutBorderLeftWidth::const_px(1))));
422
9890
    v.push(Cond::simple(P::const_border_right_width(LayoutBorderRightWidth::const_px(1))));
423
9890
    v.push(Cond::simple(P::const_border_bottom_width(LayoutBorderBottomWidth::const_px(1))));
424
9890
    v.push(Cond::simple(P::const_border_top_style(StyleBorderTopStyle { inner: BorderStyle::Solid })));
425
9890
    v.push(Cond::simple(P::const_border_left_style(StyleBorderLeftStyle { inner: BorderStyle::Solid })));
426
9890
    v.push(Cond::simple(P::const_border_right_style(StyleBorderRightStyle { inner: BorderStyle::Solid })));
427
9890
    v.push(Cond::simple(P::const_border_bottom_style(StyleBorderBottomStyle { inner: BorderStyle::Solid })));
428
9890
    push_border_colors(v, c);
429
9890
}
430

            
431
12362
fn push_border_colors(v: &mut Vec<Cond>, c: ColorU) {
432
12362
    v.push(Cond::simple(P::const_border_top_color(StyleBorderTopColor { inner: c })));
433
12362
    v.push(Cond::simple(P::const_border_left_color(StyleBorderLeftColor { inner: c })));
434
12362
    v.push(Cond::simple(P::const_border_right_color(StyleBorderRightColor { inner: c })));
435
12362
    v.push(Cond::simple(P::const_border_bottom_color(StyleBorderBottomColor { inner: c })));
436
12362
}
437

            
438
4944
fn push_hover_border_colors(v: &mut Vec<Cond>, c: ColorU) {
439
4944
    v.push(Cond::on_hover(P::const_border_top_color(StyleBorderTopColor { inner: c })));
440
4944
    v.push(Cond::on_hover(P::const_border_left_color(StyleBorderLeftColor { inner: c })));
441
4944
    v.push(Cond::on_hover(P::const_border_right_color(StyleBorderRightColor { inner: c })));
442
4944
    v.push(Cond::on_hover(P::const_border_bottom_color(StyleBorderBottomColor { inner: c })));
443
4944
}
444

            
445
/// Bottom border only (tab underline / ribbon bottom edge).
446
7416
fn push_bottom_border(v: &mut Vec<Cond>, c: ColorU) {
447
7416
    v.push(Cond::simple(P::const_border_bottom_width(LayoutBorderBottomWidth::const_px(1))));
448
7416
    v.push(Cond::simple(P::const_border_bottom_style(StyleBorderBottomStyle { inner: BorderStyle::Solid })));
449
7416
    v.push(Cond::simple(P::const_border_bottom_color(StyleBorderBottomColor { inner: c })));
450
7416
}
451

            
452
/// Transparent-bordered, hover-highlighted button chassis shared by large
453
/// and small ribbon buttons.
454
2472
fn push_button_chassis(v: &mut Vec<Cond>, t: &RibbonTheme) {
455
2472
    v.push(cond_border_box());
456
2472
    v.push(Cond::simple(P::const_cursor(StyleCursor::Default)));
457
2472
    v.push(cond_bg(TRANSPARENT));
458
2472
    push_box_border(v, TRANSPARENT);
459
2472
    v.push(cond_bg_hover(t.hover_bg));
460
2472
    push_hover_border_colors(v, t.hover_border);
461
2472
    v.push(cond_bg_active(t.pressed_bg));
462
2472
}
463

            
464
1236
fn theme_container(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
465
1236
    let mut v = vec![
466
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
467
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
468
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
469
1236
        Cond::simple(P::const_font_family(SYSTEM_UI_FAMILY)),
470
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(12))),
471
1236
        cond_bg(t.chrome_bg),
472
    ];
473
1236
    push_bottom_border(&mut v, t.border);
474
1236
    CssPropertyWithConditionsVec::from_vec(v)
475
1236
}
476

            
477
1236
fn theme_tab_bar(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
478
1236
    CssPropertyWithConditionsVec::from_vec(vec![
479
1236
        cond_border_box(),
480
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
481
        // Replaced by the full-width mobile tab button on phones. The
482
        // conditional MUST come after the unconditional value: inline
483
        // properties resolve last-match-wins.
484
1236
        only_on_mobile(P::const_display(LayoutDisplay::None)),
485
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
486
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
487
1236
        Cond::simple(P::const_height(LayoutHeight::const_px(26))),
488
1236
        cond_bg(t.chrome_bg),
489
    ])
490
1236
}
491

            
492
1236
fn theme_app_button(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
493
1236
    let mut v: Vec<Cond> = vec![
494
1236
        cond_border_box(),
495
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
496
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
497
1236
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
498
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
499
    ];
500
1236
    v.push(Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })));
501
1236
    push_padding(&mut v, 7, 17, 7, 17);
502
1236
    v.push(cond_bg(t.accent));
503
1236
    v.push(cond_text_color(t.accent_text));
504
1236
    v.push(Cond::simple(P::const_font_size(StyleFontSize::const_px(12))));
505
1236
    v.push(Cond::simple(P::const_cursor(StyleCursor::Pointer)));
506
1236
    v.push(Cond::simple(P::user_select(StyleUserSelect::None)));
507
1236
    v.push(cond_bg_hover(t.accent_hover));
508
1236
    CssPropertyWithConditionsVec::from_vec(v)
509
1236
}
510

            
511
1236
fn theme_tab(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
512
1236
    let mut v: Vec<Cond> = vec![
513
1236
        cond_border_box(),
514
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
515
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
516
1236
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
517
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
518
    ];
519
1236
    v.push(Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })));
520
1236
    push_padding(&mut v, 7, 13, 6, 13);
521
1236
    v.push(Cond::simple(P::const_cursor(StyleCursor::Pointer)));
522
1236
    v.push(Cond::simple(P::user_select(StyleUserSelect::None)));
523
1236
    v.push(cond_text_color(t.text));
524
1236
    v.push(cond_bg(t.chrome_bg));
525
1236
    push_bottom_border(&mut v, t.border);
526
1236
    v.push(Cond::on_hover(P::const_text_color(StyleTextColor { inner: t.accent })));
527
1236
    CssPropertyWithConditionsVec::from_vec(v)
528
1236
}
529

            
530
1236
fn theme_tab_active(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
531
1236
    let mut v: Vec<Cond> = vec![
532
1236
        cond_border_box(),
533
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
534
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
535
1236
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
536
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
537
    ];
538
1236
    v.push(Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })));
539
1236
    push_padding(&mut v, 6, 12, 6, 12);
540
1236
    v.push(Cond::simple(P::user_select(StyleUserSelect::None)));
541
1236
    v.push(cond_text_color(t.accent));
542
1236
    v.push(cond_bg(t.chrome_bg));
543
1236
    push_box_border(&mut v, t.border);
544
    // Erase the underline below the active tab: the bottom border matches
545
    // the chrome so the tab visually merges with the ribbon content.
546
1236
    v.push(Cond::simple(P::const_border_bottom_color(StyleBorderBottomColor {
547
1236
        inner: t.chrome_bg,
548
1236
    })));
549
1236
    CssPropertyWithConditionsVec::from_vec(v)
550
1236
}
551

            
552
1236
fn theme_tab_filler(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
553
1236
    let mut v = vec![Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1)))];
554
1236
    push_bottom_border(&mut v, t.border);
555
1236
    CssPropertyWithConditionsVec::from_vec(v)
556
1236
}
557

            
558
1236
fn theme_content(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
559
1236
    CssPropertyWithConditionsVec::from_vec(vec![
560
1236
        cond_border_box(),
561
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
562
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
563
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
564
1236
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
565
1236
        Cond::simple(P::const_height(LayoutHeight::const_px(92))),
566
1236
        cond_bg(t.chrome_bg),
567
    ])
568
1236
}
569

            
570
1236
fn theme_group(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
571
1236
    CssPropertyWithConditionsVec::from_vec(vec![
572
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
573
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
574
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
575
1236
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
576
1236
        Cond::simple(P::const_padding_top(LayoutPaddingTop::const_px(3))),
577
1236
        Cond::simple(P::const_padding_left(LayoutPaddingLeft::const_px(2))),
578
1236
        Cond::simple(P::const_padding_right(LayoutPaddingRight::const_px(2))),
579
1236
        Cond::simple(P::const_border_right_width(LayoutBorderRightWidth::const_px(1))),
580
1236
        Cond::simple(P::const_border_right_style(StyleBorderRightStyle { inner: BorderStyle::Solid })),
581
1236
        Cond::simple(P::const_border_right_color(StyleBorderRightColor { inner: t.separator })),
582
    ])
583
1236
}
584

            
585
1236
fn theme_group_label(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
586
1236
    CssPropertyWithConditionsVec::from_vec(vec![
587
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
588
1236
        Cond::simple(P::const_text_align(StyleTextAlign::Center)),
589
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(11))),
590
1236
        cond_text_color(t.label),
591
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
592
    ])
593
1236
}
594

            
595
1236
fn theme_launcher_button(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
596
1236
    let mut v = vec![
597
1236
        cond_border_box(),
598
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
599
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
600
1236
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
601
1236
        Cond::simple(P::const_justify_content(LayoutJustifyContent::Center)),
602
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
603
1236
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
604
1236
        Cond::simple(P::const_width(LayoutWidth::const_px(16))),
605
1236
        Cond::simple(P::const_height(LayoutHeight::const_px(14))),
606
    ];
607
1236
    push_padding(&mut v, 0, 0, 0, 0);
608
1236
    v.push(Cond::simple(P::const_cursor(StyleCursor::Default)));
609
1236
    v.push(cond_bg(TRANSPARENT));
610
1236
    push_box_border(&mut v, TRANSPARENT);
611
1236
    v.push(cond_bg_hover(t.hover_bg));
612
1236
    push_hover_border_colors(&mut v, t.hover_border);
613
1236
    CssPropertyWithConditionsVec::from_vec(v)
614
1236
}
615

            
616
1236
fn theme_launcher_icon(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
617
1236
    CssPropertyWithConditionsVec::from_vec(vec![
618
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(11))),
619
1236
        cond_text_color(t.label),
620
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
621
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
622
    ])
623
1236
}
624

            
625
1236
fn theme_separator(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
626
1236
    CssPropertyWithConditionsVec::from_vec(vec![
627
1236
        Cond::simple(P::const_width(LayoutWidth::const_px(1))),
628
1236
        Cond::simple(P::const_height(LayoutHeight::const_px(22))),
629
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
630
1236
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
631
1236
        Cond::simple(P::const_margin_left(LayoutMarginLeft::const_px(3))),
632
1236
        Cond::simple(P::const_margin_right(LayoutMarginRight::const_px(3))),
633
1236
        cond_bg(t.separator),
634
    ])
635
1236
}
636

            
637
1236
fn theme_large_button(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
638
1236
    let mut v = vec![
639
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
640
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
641
1236
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
642
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
643
1236
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
644
1236
        Cond::simple(P::const_height(LayoutHeight::const_px(66))),
645
1236
        Cond::simple(P::const_min_width(LayoutMinWidth::const_px(44))),
646
    ];
647
1236
    push_padding(&mut v, 3, 7, 3, 7);
648
1236
    v.push(Cond::simple(P::const_margin_right(LayoutMarginRight::const_px(1))));
649
1236
    push_button_chassis(&mut v, t);
650
1236
    CssPropertyWithConditionsVec::from_vec(v)
651
1236
}
652

            
653
1236
fn theme_large_icon(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
654
1236
    CssPropertyWithConditionsVec::from_vec(vec![
655
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(32))),
656
1236
        cond_text_color(t.icon),
657
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
658
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
659
    ])
660
1236
}
661

            
662
1236
fn theme_large_label(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
663
1236
    CssPropertyWithConditionsVec::from_vec(vec![
664
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(12))),
665
1236
        cond_text_color(t.text),
666
1236
        Cond::simple(P::const_text_align(StyleTextAlign::Center)),
667
1236
        Cond::simple(P::const_margin_top(LayoutMarginTop::const_px(3))),
668
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
669
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
670
    ])
671
1236
}
672

            
673
1236
fn theme_small_button(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
674
1236
    let mut v = vec![
675
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
676
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
677
1236
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
678
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
679
1236
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
680
1236
        Cond::simple(P::const_height(LayoutHeight::const_px(22))),
681
    ];
682
1236
    push_padding(&mut v, 1, 3, 1, 3);
683
1236
    push_button_chassis(&mut v, t);
684
1236
    CssPropertyWithConditionsVec::from_vec(v)
685
1236
}
686

            
687
1236
fn theme_small_icon(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
688
1236
    CssPropertyWithConditionsVec::from_vec(vec![
689
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(16))),
690
1236
        cond_text_color(t.icon),
691
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
692
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
693
    ])
694
1236
}
695

            
696
1236
fn theme_small_label(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
697
1236
    CssPropertyWithConditionsVec::from_vec(vec![
698
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(12))),
699
1236
        cond_text_color(t.text),
700
1236
        Cond::simple(P::const_margin_left(LayoutMarginLeft::const_px(5))),
701
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
702
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
703
    ])
704
1236
}
705

            
706
1236
fn theme_arrow_icon(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
707
1236
    CssPropertyWithConditionsVec::from_vec(vec![
708
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(14))),
709
1236
        cond_text_color(t.label),
710
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
711
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
712
    ])
713
1236
}
714

            
715
/// Appended to a button's container style when [`RibbonButton::toggled`] is
716
/// set. Inline properties resolve last-wins, so these override the base.
717
1236
fn theme_checked(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
718
1236
    let mut v = vec![cond_bg(t.checked_bg)];
719
1236
    push_border_colors(&mut v, t.hover_border);
720
1236
    CssPropertyWithConditionsVec::from_vec(v)
721
1236
}
722

            
723
1236
fn theme_gallery_frame(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
724
1236
    let mut v = vec![
725
1236
        cond_border_box(),
726
1236
        Cond::simple(P::const_min_width(LayoutMinWidth::const_px(137))),
727
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
728
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
729
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
730
1236
        Cond::simple(P::const_height(LayoutHeight::const_px(68))),
731
        // The frame IS the gallery viewport (like classic office suites): overflow hidden
732
        // both clips partially-visible cells and zeroes the frame's
733
        // automatic minimum size so it yields space to rigid groups.
734
        // (taffy 0.10 only collapses the minimum for DIRECT scroll
735
        // containers — see layout/tests/flex_intrinsic_text.rs.)
736
1236
        Cond::simple(P::const_overflow_x(LayoutOverflow::Hidden)),
737
1236
        Cond::simple(P::const_overflow_y(LayoutOverflow::Hidden)),
738
1236
        cond_bg(t.chrome_bg),
739
    ];
740
1236
    push_box_border(&mut v, t.border);
741
1236
    CssPropertyWithConditionsVec::from_vec(v)
742
1236
}
743

            
744
1236
fn theme_gallery_cell(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
745
1236
    let mut v = vec![
746
1236
        cond_border_box(),
747
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
748
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
749
1236
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
750
1236
        Cond::simple(P::const_justify_content(LayoutJustifyContent::Center)),
751
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
752
1236
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
753
1236
        Cond::simple(P::const_width(LayoutWidth::const_px(120))),
754
    ];
755
1236
    push_padding(&mut v, 2, 6, 2, 6);
756
1236
    v.push(Cond::simple(P::const_cursor(StyleCursor::Default)));
757
1236
    v.push(Cond::simple(P::user_select(StyleUserSelect::None)));
758
1236
    push_box_border(&mut v, TRANSPARENT);
759
    // Cells are divided by a thin rule on their right edge.
760
1236
    v.push(Cond::simple(P::const_border_right_color(StyleBorderRightColor {
761
1236
        inner: t.separator,
762
1236
    })));
763
1236
    v.push(cond_bg_hover(t.hover_bg));
764
1236
    push_hover_border_colors(&mut v, t.hover_border);
765
1236
    CssPropertyWithConditionsVec::from_vec(v)
766
1236
}
767

            
768
/// Appended to [`RibbonStyle::gallery_cell_style`] for the selected cell.
769
1236
fn theme_gallery_cell_selected(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
770
1236
    let mut v = vec![cond_bg(t.selected_bg)];
771
1236
    push_border_colors(&mut v, t.hover_border);
772
1236
    CssPropertyWithConditionsVec::from_vec(v)
773
1236
}
774

            
775
1236
fn theme_gallery_cell_label(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
776
1236
    CssPropertyWithConditionsVec::from_vec(vec![
777
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(11))),
778
1236
        cond_text_color(t.text),
779
1236
        Cond::simple(P::const_margin_top(LayoutMarginTop::const_px(2))),
780
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
781
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
782
    ])
783
1236
}
784

            
785
1236
fn theme_gallery_spinner(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
786
1236
    CssPropertyWithConditionsVec::from_vec(vec![
787
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
788
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
789
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
790
1236
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
791
1236
        Cond::simple(P::const_width(LayoutWidth::const_px(15))),
792
1236
        Cond::simple(P::const_border_left_width(LayoutBorderLeftWidth::const_px(1))),
793
1236
        Cond::simple(P::const_border_left_style(StyleBorderLeftStyle { inner: BorderStyle::Solid })),
794
1236
        Cond::simple(P::const_border_left_color(StyleBorderLeftColor { inner: t.separator })),
795
    ])
796
1236
}
797

            
798
/// The gallery wrapper is the positioning context for the expansion panel;
799
/// it is otherwise transparent and behaves exactly like the bare frame.
800
static GALLERY_WRAPPER_STYLE: &[Cond] = &[
801
    Cond::simple(P::const_display(LayoutDisplay::Flex)),
802
    Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
803
    Cond::simple(P::const_position(LayoutPosition::Relative)),
804
    Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
805
    Cond::simple(P::const_min_width(LayoutMinWidth::const_px(137))),
806
];
807

            
808
/// The "More" expansion panel: an absolutely-positioned wrapped grid of every
809
/// gallery cell, hidden until the More button toggles its `display`.
810
1236
fn theme_gallery_panel(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
811
1236
    let mut v = vec![
812
1236
        Cond::simple(P::const_display(LayoutDisplay::None)),
813
1236
        Cond::simple(P::const_position(LayoutPosition::Absolute)),
814
1236
        Cond::simple(P::const_top(LayoutTop::const_px(68))),
815
1236
        Cond::simple(P::const_left(LayoutLeft::const_px(0))),
816
1236
        Cond::simple(P::const_width(LayoutWidth::const_px(612))),
817
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
818
1236
        Cond::simple(P::const_flex_wrap(LayoutFlexWrap::Wrap)),
819
1236
        cond_bg(t.chrome_bg),
820
    ];
821
1236
    push_box_border(&mut v, t.border);
822
1236
    CssPropertyWithConditionsVec::from_vec(v)
823
1236
}
824

            
825
1236
fn theme_gallery_spinner_button(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
826
1236
    let mut v = vec![
827
1236
        cond_border_box(),
828
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
829
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
830
1236
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
831
1236
        Cond::simple(P::const_justify_content(LayoutJustifyContent::Center)),
832
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
833
1236
        Cond::simple(P::const_width(LayoutWidth::const_px(14))),
834
    ];
835
1236
    push_padding(&mut v, 0, 0, 0, 0);
836
1236
    v.push(Cond::simple(P::const_cursor(StyleCursor::Default)));
837
1236
    v.push(cond_bg(TRANSPARENT));
838
1236
    v.push(Cond::simple(P::const_border_top_width(LayoutBorderTopWidth::const_px(0))));
839
1236
    v.push(Cond::simple(P::const_border_left_width(LayoutBorderLeftWidth::const_px(0))));
840
1236
    v.push(Cond::simple(P::const_border_right_width(LayoutBorderRightWidth::const_px(0))));
841
1236
    v.push(Cond::simple(P::const_border_bottom_width(LayoutBorderBottomWidth::const_px(0))));
842
1236
    v.push(cond_bg_hover(t.hover_bg));
843
1236
    CssPropertyWithConditionsVec::from_vec(v)
844
1236
}
845

            
846
1236
fn theme_gallery_spinner_icon(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
847
1236
    CssPropertyWithConditionsVec::from_vec(vec![
848
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(12))),
849
1236
        cond_text_color(t.label),
850
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
851
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
852
    ])
853
1236
}
854

            
855
/// Office-2013-look combobox parts, injected by [`RibbonStyle::styled_combo_box`].
856
2
fn theme_combo_wrapper_base(_t: &RibbonTheme) -> Vec<Cond> {
857
2
    vec![
858
2
        Cond::simple(P::const_display(LayoutDisplay::InlineBlock)),
859
2
        Cond::simple(P::const_position(LayoutPosition::Relative)),
860
2
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
861
2
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
862
2
        Cond::simple(P::const_margin_right(LayoutMarginRight::const_px(2))),
863
2
        Cond::simple(P::const_font_size(StyleFontSize::const_px(12))),
864
2
        Cond::simple(P::const_font_family(SYSTEM_UI_FAMILY)),
865
    ]
866
2
}
867

            
868
2
fn theme_combo_field(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
869
2
    let mut v = vec![
870
2
        cond_border_box(),
871
2
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
872
2
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
873
2
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
874
2
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
875
2
        Cond::simple(P::const_height(LayoutHeight::const_px(22))),
876
    ];
877
2
    push_padding(&mut v, 0, 2, 0, 5);
878
2
    v.push(Cond::simple(P::const_cursor(StyleCursor::Text)));
879
2
    v.push(cond_bg(t.chrome_bg));
880
2
    v.push(cond_text_color(t.text));
881
2
    push_box_border(&mut v, t.field_border);
882
2
    v.push(Cond::on_focus(P::const_border_top_color(StyleBorderTopColor { inner: t.accent })));
883
2
    v.push(Cond::on_focus(P::const_border_left_color(StyleBorderLeftColor { inner: t.accent })));
884
2
    v.push(Cond::on_focus(P::const_border_right_color(StyleBorderRightColor { inner: t.accent })));
885
2
    v.push(Cond::on_focus(P::const_border_bottom_color(StyleBorderBottomColor { inner: t.accent })));
886
2
    CssPropertyWithConditionsVec::from_vec(v)
887
2
}
888

            
889
2
fn theme_combo_arrow(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
890
2
    CssPropertyWithConditionsVec::from_vec(vec![
891
2
        Cond::simple(P::const_font_size(StyleFontSize::const_px(14))),
892
2
        cond_text_color(t.label),
893
2
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
894
2
        Cond::simple(P::user_select(StyleUserSelect::None)),
895
    ])
896
2
}
897

            
898
// -- Mobile part styles --
899
//
900
// Touch targets follow the platform minimum (44px). The desktop tab strip
901
// and the mobile tab button are mutually exclusive via the viewport
902
// condition, so exactly one is ever visible.
903

            
904
/// The full-width tab button that replaces the tab strip on phones. Shows the
905
/// ACTIVE tab's label plus a chevron that opens the tab overlay; double
906
/// tapping it collapses the ribbon exactly like double clicking a desktop tab.
907
1236
fn theme_mobile_tab_button(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
908
1236
    let mut v: Vec<Cond> = mobile_only_visibility(LayoutDisplay::Flex).to_vec();
909
1236
    v.push(cond_border_box());
910
1236
    v.push(Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)));
911
1236
    v.push(Cond::simple(P::const_align_items(LayoutAlignItems::Center)));
912
1236
    v.push(Cond::simple(P::const_height(LayoutHeight::const_px(48))));
913
1236
    v.push(Cond::simple(P::const_width(LayoutWidth::Px(PixelValue::const_percent(100)))));
914
1236
    push_padding(&mut v, 0, 12, 0, 16);
915
1236
    v.push(Cond::simple(P::const_font_size(StyleFontSize::const_px(17))));
916
1236
    v.push(cond_text_color(t.accent));
917
1236
    v.push(cond_bg(t.chrome_bg));
918
1236
    push_bottom_border(&mut v, t.border);
919
1236
    v.push(Cond::simple(P::const_cursor(StyleCursor::Pointer)));
920
1236
    v.push(Cond::simple(P::user_select(StyleUserSelect::None)));
921
1236
    CssPropertyWithConditionsVec::from_vec(v)
922
1236
}
923

            
924
/// The active tab's label inside the mobile tab button.
925
1236
fn theme_mobile_tab_label(_t: &RibbonTheme) -> CssPropertyWithConditionsVec {
926
1236
    CssPropertyWithConditionsVec::from_vec(vec![
927
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
928
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
929
    ])
930
1236
}
931

            
932
/// Chevron on the mobile tab button.
933
1236
fn theme_mobile_tab_arrow(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
934
1236
    CssPropertyWithConditionsVec::from_vec(vec![
935
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(24))),
936
1236
        cond_text_color(t.accent),
937
1236
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
938
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
939
    ])
940
1236
}
941

            
942
/// Full-screen overlay listing every tab; opened by the mobile tab button.
943
1236
fn theme_mobile_tab_overlay(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
944
1236
    let mut v = vec![
945
        // Hidden until the button opens it (on ANY viewport: the overlay is
946
        // only reachable through the mobile button).
947
1236
        Cond::simple(P::const_display(LayoutDisplay::None)),
948
1236
        cond_border_box(),
949
1236
        Cond::simple(P::const_position(LayoutPosition::Absolute)),
950
1236
        Cond::simple(P::const_top(LayoutTop::const_px(0))),
951
1236
        Cond::simple(P::const_left(LayoutLeft::const_px(0))),
952
1236
        Cond::simple(P::const_width(LayoutWidth::Px(PixelValue::const_percent(100)))),
953
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
954
1236
        cond_bg(t.chrome_bg),
955
    ];
956
1236
    push_box_border(&mut v, t.border);
957
1236
    CssPropertyWithConditionsVec::from_vec(v)
958
1236
}
959

            
960
/// One row of the mobile tab overlay - a full-width 48px touch target.
961
1236
fn theme_mobile_tab_overlay_item(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
962
1236
    let mut v = vec![
963
1236
        cond_border_box(),
964
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
965
1236
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
966
1236
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
967
1236
        Cond::simple(P::const_height(LayoutHeight::const_px(48))),
968
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(17))),
969
1236
        cond_text_color(t.text),
970
1236
        Cond::simple(P::const_cursor(StyleCursor::Pointer)),
971
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
972
    ];
973
1236
    push_padding(&mut v, 0, 16, 0, 16);
974
1236
    push_bottom_border(&mut v, t.separator);
975
1236
    v.push(cond_bg_hover(t.hover_bg));
976
1236
    CssPropertyWithConditionsVec::from_vec(v)
977
1236
}
978

            
979
/// The scrollable list of GROUP names shown beside the visible group on
980
/// phones. Sits on the user's dominant-hand side (see [`Handedness`]).
981
1236
fn theme_mobile_group_list(t: &RibbonTheme, left_handed: bool) -> CssPropertyWithConditionsVec {
982
1236
    let mut v: Vec<Cond> = mobile_only_visibility(LayoutDisplay::Flex).to_vec();
983
1236
    v.push(cond_border_box());
984
1236
    v.push(Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)));
985
1236
    v.push(Cond::simple(P::const_width(LayoutWidth::const_px(116))));
986
1236
    v.push(Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))));
987
1236
    v.push(Cond::simple(P::const_flex_shrink(LayoutFlexShrink {
988
1236
        inner: FloatValue::const_new(0),
989
1236
    })));
990
1236
    v.push(Cond::simple(P::const_overflow_y(LayoutOverflow::Scroll)));
991
1236
    v.push(Cond::simple(P::const_overflow_x(LayoutOverflow::Hidden)));
992
1236
    v.push(cond_bg(t.chrome_bg));
993
    // The list hugs the dominant hand: a border on the side that faces the
994
    // content, so the divider reads correctly whichever side it is on.
995
1236
    if left_handed {
996
3
        v.push(Cond::simple(P::const_border_right_width(LayoutBorderRightWidth::const_px(1))));
997
3
        v.push(Cond::simple(P::const_border_right_style(StyleBorderRightStyle {
998
3
            inner: BorderStyle::Solid,
999
3
        })));
3
        v.push(Cond::simple(P::const_border_right_color(StyleBorderRightColor {
3
            inner: t.separator,
3
        })));
1233
    } else {
1233
        v.push(Cond::simple(P::const_border_left_width(LayoutBorderLeftWidth::const_px(1))));
1233
        v.push(Cond::simple(P::const_border_left_style(StyleBorderLeftStyle {
1233
            inner: BorderStyle::Solid,
1233
        })));
1233
        v.push(Cond::simple(P::const_border_left_color(StyleBorderLeftColor {
1233
            inner: t.separator,
1233
        })));
1233
    }
1236
    CssPropertyWithConditionsVec::from_vec(v)
1236
}
/// One entry of the mobile group list.
1236
fn theme_mobile_group_list_item(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
1236
    let mut v = vec![
1236
        cond_border_box(),
1236
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
1236
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
1236
        Cond::simple(P::const_height(LayoutHeight::const_px(44))),
1236
        Cond::simple(P::const_font_size(StyleFontSize::const_px(15))),
1236
        cond_text_color(t.text),
1236
        Cond::simple(P::const_cursor(StyleCursor::Pointer)),
1236
        Cond::simple(P::user_select(StyleUserSelect::None)),
    ];
1236
    push_padding(&mut v, 0, 10, 0, 12);
1236
    push_bottom_border(&mut v, t.separator);
1236
    v.push(cond_bg_hover(t.hover_bg));
1236
    CssPropertyWithConditionsVec::from_vec(v)
1236
}
/// The selected entry of the mobile group list (appended, last-wins).
1236
fn theme_mobile_group_list_item_selected(t: &RibbonTheme) -> CssPropertyWithConditionsVec {
1236
    CssPropertyWithConditionsVec::from_vec(vec![
1236
        cond_bg(t.selected_bg),
1236
        cond_text_color(t.accent),
    ])
1236
}
// -- Classes --
static CLS_RIBBON: &[IdOrClass] = &[Class(AzString::from_const_str("__azul-native-ribbon"))];
static CLS_TAB_BAR: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-tabbar"))];
static CLS_APP_BUTTON: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-appbutton"))];
static CLS_TAB: &[IdOrClass] = &[Class(AzString::from_const_str("__azul-native-ribbon-tab"))];
static CLS_TAB_ACTIVE: &[IdOrClass] = &[
    Class(AzString::from_const_str("__azul-native-ribbon-tab")),
    Class(AzString::from_const_str("__azul-native-ribbon-tab-active")),
];
static CLS_TAB_FILLER: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-tab-filler"))];
static CLS_CONTENT: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-content"))];
static CLS_GROUP: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-group"))];
static CLS_GROUP_ITEMS: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-group-items"))];
static CLS_GROUP_FOOTER: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-group-footer"))];
static CLS_GROUP_LABEL: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-group-label"))];
static CLS_FOOTER_SPACER: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-footer-spacer"))];
static CLS_COLUMN: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-column"))];
static CLS_ROW: &[IdOrClass] = &[Class(AzString::from_const_str("__azul-native-ribbon-row"))];
static CLS_SEPARATOR: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-separator"))];
static CLS_GALLERY: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-gallery"))];
static CLS_GALLERY_STRIP: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-gallery-strip"))];
static CLS_GALLERY_CELL: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-gallery-cell"))];
static CLS_GALLERY_CELL_SELECTED: &[IdOrClass] = &[
    Class(AzString::from_const_str("__azul-native-ribbon-gallery-cell")),
    Class(AzString::from_const_str("__azul-native-ribbon-gallery-cell-selected")),
];
/// Class names the handlers resolve their targets by (see
/// `ancestor_with_class`), kept next to the `IdOrClass` tables that emit them.
const GALLERY_WRAPPER_CLASS: &str = "__azul-native-ribbon-gallery-wrapper";
const GALLERY_CELL_CLASS: &str = "__azul-native-ribbon-gallery-cell";
const RIBBON_TAB_CLASS: &str = "__azul-native-ribbon-tab";
const MOBILE_TAB_BUTTON_CLASS: &str = "__azul-native-ribbon-mobile-tab";
const RIBBON_CONTAINER_CLASS: &str = "__azul-native-ribbon";
const MOBILE_GROUP_LIST_ITEM_CLASS: &str = "__azul-native-ribbon-mobile-group-list-item";
const RIBBON_CONTENT_CLASS: &str = "__azul-native-ribbon-content";
static CLS_MOBILE_TAB_BUTTON: &[IdOrClass] =
    &[Class(AzString::from_const_str(MOBILE_TAB_BUTTON_CLASS))];
static CLS_MOBILE_TAB_OVERLAY: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-mobile-tab-overlay"))];
static CLS_MOBILE_TAB_OVERLAY_ITEM: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-mobile-tab-overlay-item"))];
static CLS_MOBILE_GROUP_LIST: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-mobile-group-list"))];
static CLS_MOBILE_BAND: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-mobile-band"))];
static CLS_MOBILE_GROUP_LIST_ITEM: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-mobile-group-list-item"))];
static CLS_GALLERY_MORE: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-gallery-more"))];
static CLS_GALLERY_WRAPPER: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-gallery-wrapper"))];
static CLS_GALLERY_PANEL: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-gallery-panel"))];
static CLS_GALLERY_SPINNER: &[IdOrClass] =
    &[Class(AzString::from_const_str("__azul-native-ribbon-gallery-spinner"))];
// -- Style bundle --
/// Every visual part of the ribbon chrome as a replaceable property list.
///
/// The default ([`RibbonStyle::office_2013`]) reproduces the the Office-2013-era look look:
/// white chrome, #2B579A accents, #CDE6F7 hover fills. Each field is applied
/// to exactly one DOM part; replace any of them to re-theme that part.
/// Fields named `*_style` fully replace the part's style; [`Self::checked_style`]
/// and [`Self::gallery_cell_selected_style`] are *appended* to the base button /
/// cell style (inline CSS resolves last-wins, so appended properties override).
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct RibbonStyle {
    /// The palette this style bundle was derived from. Kept for
    /// [`Self::styled_combo_box`] and for consumers deriving matching
    /// custom parts.
    pub theme: RibbonTheme,
    /// Root container (vertical: tab bar over content).
    pub container_style: CssPropertyWithConditionsVec,
    /// The horizontal tab strip.
    pub tab_bar_style: CssPropertyWithConditionsVec,
    /// The blue application button ("FILE").
    pub app_button_style: CssPropertyWithConditionsVec,
    /// An inactive tab header.
    pub tab_style: CssPropertyWithConditionsVec,
    /// The active tab header.
    pub tab_active_style: CssPropertyWithConditionsVec,
    /// The filler segment after the last tab (carries the underline).
    pub tab_filler_style: CssPropertyWithConditionsVec,
    /// The content band below the tab strip (horizontal group list).
    pub content_style: CssPropertyWithConditionsVec,
    /// One group (vertical: items over footer), incl. the right separator.
    pub group_style: CssPropertyWithConditionsVec,
    /// The item area of a group.
    pub group_items_style: CssPropertyWithConditionsVec,
    /// The footer row of a group (label + dialog launcher).
    pub group_footer_style: CssPropertyWithConditionsVec,
    /// The centered group caption.
    pub group_label_style: CssPropertyWithConditionsVec,
    /// Invisible spacer balancing the launcher so the caption stays centered.
    pub footer_spacer_style: CssPropertyWithConditionsVec,
    /// Container style injected into the dialog-launcher [`Button`].
    pub launcher_button_style: CssPropertyWithConditionsVec,
    /// Icon style injected into the dialog-launcher [`Button`].
    pub launcher_icon_style: CssPropertyWithConditionsVec,
    /// A [`RibbonColumn`] packing box.
    pub column_style: CssPropertyWithConditionsVec,
    /// A [`RibbonRow`] packing box.
    pub row_style: CssPropertyWithConditionsVec,
    /// A [`RibbonItem::Separator`] rule.
    pub separator_style: CssPropertyWithConditionsVec,
    /// Container style injected into large-button [`Button`]s.
    pub large_button_style: CssPropertyWithConditionsVec,
    /// Icon style injected into large-button [`Button`]s.
    pub large_icon_style: CssPropertyWithConditionsVec,
    /// Label style injected into large-button [`Button`]s.
    pub large_label_style: CssPropertyWithConditionsVec,
    /// Container style injected into small-button [`Button`]s.
    pub small_button_style: CssPropertyWithConditionsVec,
    /// Icon style injected into small-button [`Button`]s.
    pub small_icon_style: CssPropertyWithConditionsVec,
    /// Label style injected into small-button [`Button`]s.
    pub small_label_style: CssPropertyWithConditionsVec,
    /// Style of the drop-down arrow glyph on Menu/Split buttons.
    pub arrow_icon_style: CssPropertyWithConditionsVec,
    /// APPENDED to the button container when [`RibbonButton::toggled`] is set.
    pub checked_style: CssPropertyWithConditionsVec,
    /// The gallery outer frame.
    pub gallery_frame_style: CssPropertyWithConditionsVec,
    /// The horizontal cell strip inside the gallery frame.
    pub gallery_strip_style: CssPropertyWithConditionsVec,
    /// One gallery cell.
    pub gallery_cell_style: CssPropertyWithConditionsVec,
    /// APPENDED to the selected gallery cell.
    pub gallery_cell_selected_style: CssPropertyWithConditionsVec,
    /// The name label under a gallery cell preview.
    pub gallery_cell_label_style: CssPropertyWithConditionsVec,
    /// The vertical spinner column on the gallery's right edge.
    pub gallery_spinner_style: CssPropertyWithConditionsVec,
    /// Positioning context wrapping the gallery frame + expansion panel.
    pub gallery_wrapper_style: CssPropertyWithConditionsVec,
    /// The expansion panel shown by the gallery's "More" button.
    pub gallery_panel_style: CssPropertyWithConditionsVec,
    /// Full-width tab button shown INSTEAD of the tab strip on phones.
    pub mobile_tab_button_style: CssPropertyWithConditionsVec,
    /// Active-tab label inside the mobile tab button.
    pub mobile_tab_label_style: CssPropertyWithConditionsVec,
    /// Chevron on the mobile tab button.
    pub mobile_tab_arrow_style: CssPropertyWithConditionsVec,
    /// Full-screen tab picker opened by the mobile tab button.
    pub mobile_tab_overlay_style: CssPropertyWithConditionsVec,
    /// One row of the mobile tab picker.
    pub mobile_tab_overlay_item_style: CssPropertyWithConditionsVec,
    /// Scrollable group list shown beside the visible group on phones.
    pub mobile_group_list_style: CssPropertyWithConditionsVec,
    /// One entry of the mobile group list.
    pub mobile_group_list_item_style: CssPropertyWithConditionsVec,
    /// APPENDED to the selected mobile group-list entry.
    pub mobile_group_list_item_selected_style: CssPropertyWithConditionsVec,
    /// Container style injected into the three spinner [`Button`]s.
    pub gallery_spinner_button_style: CssPropertyWithConditionsVec,
    /// Icon style injected into the three spinner [`Button`]s.
    pub gallery_spinner_icon_style: CssPropertyWithConditionsVec,
}
impl RibbonStyle {
    /// The the Office-2013-era look look (white chrome, #2B579A accents) - the default.
    #[must_use]
1228
    pub fn office_2013() -> Self {
1228
        Self::from_theme(RibbonTheme::office_2013())
1228
    }
    /// Derives every part style from the given palette. This is the styling
    /// override API: build a [`RibbonTheme`] (or start from a preset), then
    /// replace individual `*_style` fields for finer control.
    #[must_use]
1231
    pub fn from_theme(theme: RibbonTheme) -> Self {
1231
        Self::from_theme_handed(theme, Handedness::RightHanded)
1231
    }
    /// [`Self::from_theme`] with an explicit hand: the mobile group list sits
    /// on the dominant-hand side so the thumb reaches it. Independent of text
    /// direction - see [`Handedness`].
    #[must_use]
1236
    pub fn from_theme_handed(theme: RibbonTheme, handedness: Handedness) -> Self {
1236
        let left_handed = matches!(handedness, Handedness::LeftHanded);
1236
        let theme = &theme;
1236
        Self {
1236
            theme: *theme,
1236
            container_style: theme_container(theme),
1236
            tab_bar_style: theme_tab_bar(theme),
1236
            app_button_style: theme_app_button(theme),
1236
            tab_style: theme_tab(theme),
1236
            tab_active_style: theme_tab_active(theme),
1236
            tab_filler_style: theme_tab_filler(theme),
1236
            content_style: theme_content(theme),
1236
            group_style: theme_group(theme),
1236
            group_items_style: CssPropertyWithConditionsVec::from_const_slice(GROUP_ITEMS_STYLE),
1236
            group_footer_style: CssPropertyWithConditionsVec::from_const_slice(GROUP_FOOTER_STYLE),
1236
            group_label_style: theme_group_label(theme),
1236
            footer_spacer_style: CssPropertyWithConditionsVec::from_const_slice(FOOTER_SPACER_STYLE),
1236
            launcher_button_style: theme_launcher_button(theme),
1236
            launcher_icon_style: theme_launcher_icon(theme),
1236
            column_style: CssPropertyWithConditionsVec::from_const_slice(COLUMN_STYLE),
1236
            row_style: CssPropertyWithConditionsVec::from_const_slice(ROW_STYLE),
1236
            separator_style: theme_separator(theme),
1236
            large_button_style: theme_large_button(theme),
1236
            large_icon_style: theme_large_icon(theme),
1236
            large_label_style: theme_large_label(theme),
1236
            small_button_style: theme_small_button(theme),
1236
            small_icon_style: theme_small_icon(theme),
1236
            small_label_style: theme_small_label(theme),
1236
            arrow_icon_style: theme_arrow_icon(theme),
1236
            checked_style: theme_checked(theme),
1236
            gallery_frame_style: theme_gallery_frame(theme),
1236
            gallery_strip_style: CssPropertyWithConditionsVec::from_const_slice(GALLERY_STRIP_STYLE),
1236
            gallery_cell_style: theme_gallery_cell(theme),
1236
            gallery_cell_selected_style: theme_gallery_cell_selected(theme),
1236
            gallery_cell_label_style: theme_gallery_cell_label(theme),
1236
            gallery_spinner_style: theme_gallery_spinner(theme),
1236
            gallery_wrapper_style: CssPropertyWithConditionsVec::from_const_slice(
1236
                GALLERY_WRAPPER_STYLE,
1236
            ),
1236
            gallery_panel_style: theme_gallery_panel(theme),
1236
            mobile_tab_button_style: theme_mobile_tab_button(theme),
1236
            mobile_tab_label_style: theme_mobile_tab_label(theme),
1236
            mobile_tab_arrow_style: theme_mobile_tab_arrow(theme),
1236
            mobile_tab_overlay_style: theme_mobile_tab_overlay(theme),
1236
            mobile_tab_overlay_item_style: theme_mobile_tab_overlay_item(theme),
1236
            mobile_group_list_style: theme_mobile_group_list(theme, left_handed),
1236
            mobile_group_list_item_style: theme_mobile_group_list_item(theme),
1236
            mobile_group_list_item_selected_style: theme_mobile_group_list_item_selected(theme),
1236
            gallery_spinner_button_style: theme_gallery_spinner_button(theme),
1236
            gallery_spinner_icon_style: theme_gallery_spinner_icon(theme),
1236
        }
1236
    }
    /// Derives the ribbon style from the OS theme (see
    /// [`RibbonTheme::from_system`]). Pass `SystemStyle::detect()` for the
    /// live system look, e.g. to render a "system native" ribbon.
    #[must_use]
2
    pub fn from_system(style: SystemStyle) -> Self {
2
        let handedness = style.handedness;
2
        Self::from_theme_handed(RibbonTheme::from_system(style), handedness)
2
    }
    /// Returns a [`ComboBox`] with this ribbon's field look injected through
    /// the combobox's public style fields (flat 1px border, 22px field, 12px
    /// text - the the Office-2013-era look font-name/font-size pickers). `width` is the
    /// total field width in px. Demonstrates (and exercises) the widget
    /// style-injection API; tweak the returned combobox further by replacing
    /// any of its `*_style` fields.
    #[must_use]
2
    pub fn styled_combo_box(&self, items: StringVec, text: AzString, width: isize) -> ComboBox {
2
        let mut combo = ComboBox::new(items).with_text(text);
2
        let mut wrapper: Vec<Cond> = theme_combo_wrapper_base(&self.theme);
2
        wrapper.push(Cond::simple(P::const_width(LayoutWidth::const_px(width))));
2
        combo.wrapper_style = CssPropertyWithConditionsVec::from_vec(wrapper);
2
        combo.field_style = theme_combo_field(&self.theme);
2
        combo.text_style =
2
            CssPropertyWithConditionsVec::from_const_slice(RIBBON_COMBO_TEXT_STYLE);
2
        combo.arrow_style = theme_combo_arrow(&self.theme);
2
        combo
2
    }
}
impl Default for RibbonStyle {
1
    fn default() -> Self {
1
        Self::office_2013()
1
    }
}
// -- Data model --
/// The interactive behaviors the ribbon performs BY ITSELF, without any
/// application state. Each is the classic default and each can be turned off,
/// in which case the corresponding event is still forwarded to the app
/// callback (if any) but the ribbon does not touch its own chrome.
///
/// The state these behaviors need (collapsed / peeked / selected cell) lives
/// in a private `RefAny` minted inside [`Ribbon::dom`] — the application's
/// own data model is never involved.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct RibbonBehavior {
    /// Double-clicking a tab header collapses the content band; double
    /// clicking again restores it (office-2013: "Collapse the Ribbon").
    pub collapsible: bool,
    /// While collapsed, hovering a tab header peeks the content band and
    /// leaving it hides the band again.
    pub peek_on_hover: bool,
    /// Clicking a gallery cell moves the selection highlight without waiting
    /// for the app to re-render.
    pub auto_select_gallery: bool,
    /// The gallery's third spinner button ("More") toggles an expansion
    /// panel showing every cell.
    pub expandable_gallery: bool,
    /// On phones, tapping the tab button opens the full-screen tab picker.
    /// With this off the button is inert and the application drives tab
    /// switching itself.
    pub mobile_tab_overlay: bool,
}
impl RibbonBehavior {
    /// All classic office-suite behaviors enabled - the default.
    #[must_use]
1219
    pub const fn office_2013() -> Self {
1219
        Self {
1219
            collapsible: true,
1219
            peek_on_hover: true,
1219
            auto_select_gallery: true,
1219
            expandable_gallery: true,
1219
            mobile_tab_overlay: true,
1219
        }
1219
    }
    /// Every self-driven behavior off: the ribbon only forwards events to
    /// the application callbacks and never patches its own chrome.
    #[must_use]
6
    pub const fn inert() -> Self {
6
        Self {
6
            collapsible: false,
6
            peek_on_hover: false,
6
            auto_select_gallery: false,
6
            expandable_gallery: false,
6
            mobile_tab_overlay: false,
6
        }
6
    }
}
impl Default for RibbonBehavior {
1
    fn default() -> Self {
1
        Self::office_2013()
1
    }
}
/// Top-level ribbon widget: an optional application button, a tab strip and
/// the active tab's groups.
#[derive(Debug, Clone)]
#[repr(C)]
pub struct Ribbon {
    /// Optional application button rendered before the first tab ("FILE").
    pub app_button: OptionRibbonAppButton,
    /// Tabs displayed in the ribbon tab bar.
    pub tabs: RibbonTabVec,
    /// Index of the currently active tab.
    pub active_tab: usize,
    /// Optional callback fired when a tab is clicked (receives the tab index).
    pub on_tab_click: OptionRibbonOnTabClick,
    /// All part styles (defaults to the the Office-2013-era look look).
    pub style: RibbonStyle,
    /// Which interactions the ribbon handles by itself (defaults to the classic behavior).
    pub behavior: RibbonBehavior,
}
/// The application button at the far left of the tab strip ("FILE").
#[derive(Debug, Clone)]
#[repr(C)]
pub struct RibbonAppButton {
    /// Display label of the application button.
    pub label: AzString,
    /// Optional click callback.
    pub on_click: OptionButtonOnClick,
}
/// A single tab within a [`Ribbon`], containing a label and groups.
#[derive(Debug, Clone)]
#[repr(C)]
pub struct RibbonTab {
    /// Display label shown in the tab bar.
    pub label: AzString,
    /// Groups rendered when this tab is active.
    pub groups: RibbonGroupVec,
    /// Extra properties APPENDED to this tab header's style, after the
    /// shared [`RibbonStyle::tab_style`] / [`RibbonStyle::tab_active_style`]
    /// — so they win, and they apply in BOTH states.
    ///
    /// Empty (the default) leaves the tab looking like every other one.
    /// This is the only per-tab hook: `RibbonStyle` describes the tab
    /// STRIP, so without it a single tab could not be tinted, badged or
    /// given its own border, and telling two tabs apart in a screenshot
    /// meant reading their labels.
    pub style: CssPropertyWithConditionsVec,
}
/// A captioned group of controls within a [`RibbonTab`].
#[derive(Debug, Clone)]
#[repr(C)]
pub struct RibbonGroup {
    /// Caption shown centered under the group content.
    pub label: AzString,
    /// The controls of this group, laid out left-to-right.
    pub items: RibbonItemVec,
    /// Optional dialog-box-launcher callback; when set, a small launcher
    /// button is rendered at the right end of the caption row.
    pub launcher: OptionButtonOnClick,
    /// When set, this group absorbs the remaining ribbon width (the classic office-suite
    /// Styles gallery group stretches; the other groups are content-sized).
    pub fills_space: bool,
}
/// One control slot inside a [`RibbonGroup`].
// `#[repr(C, u8)]` — this enum crosses the C ABI and is mirrored in api.json.
// Boxing the large variant to equalise sizes would change the generated
// bindings for every language, so the size spread is deliberate.
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
#[repr(C, u8)]
pub enum RibbonItem {
    /// Full-height button: icon over label (`RibbonX` `button[size=large]`).
    LargeButton(RibbonButton),
    /// One-row button: icon beside optional label (`RibbonX` `button`).
    SmallButton(RibbonButton),
    /// Vertical packing box (`RibbonX` `box[boxStyle=vertical]`).
    Column(RibbonColumn),
    /// Horizontal packing box (`RibbonX` `box`/`buttonGroup`).
    Row(RibbonRow),
    /// Embeds the existing [`ComboBox`] widget (`RibbonX` `comboBox`).
    Combo(ComboBox),
    /// Embeds the existing [`DropDown`] widget (`RibbonX` `dropDown`).
    Drop(DropDown),
    /// Embeds the existing [`CheckBox`] widget (`RibbonX` `checkBox`).
    Check(CheckBox),
    /// In-ribbon gallery with spinner column (`RibbonX` `gallery`).
    Gallery(RibbonGallery),
    /// Thin vertical rule (`RibbonX` `separator`).
    Separator,
    /// Arbitrary user content.
    Custom(Dom),
}
/// Vertical stack of items (e.g. the Cut/Copy/Format-Painter column).
#[derive(Debug, Clone)]
#[repr(C)]
pub struct RibbonColumn {
    /// Items stacked top-to-bottom.
    pub items: RibbonItemVec,
}
/// Horizontal cluster of items (e.g. the Bold/Italic/Underline row).
#[derive(Debug, Clone)]
#[repr(C)]
pub struct RibbonRow {
    /// Items packed left-to-right.
    pub items: RibbonItemVec,
}
/// Declarative description of one ribbon button; expands to the existing
/// [`Button`] widget with ribbon styles injected.
#[derive(Debug, Clone)]
#[repr(C)]
pub struct RibbonButton {
    /// Icon name resolved via the icon provider (Material Icons ships
    /// builtin, e.g. "`content_paste`"). Empty string = no icon.
    pub icon: AzString,
    /// Button label. Empty string = icon-only button.
    pub label: AzString,
    /// Drop-down decoration: none, menu arrow or split-button arrow.
    pub arrow: RibbonArrow,
    /// Renders the button in the toggled-on state (`RibbonX` `toggleButton`).
    pub toggled: bool,
    /// Optional click callback (same family as [`Button::on_click`]).
    pub on_click: OptionButtonOnClick,
}
/// Drop-down decoration of a [`RibbonButton`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[repr(C)]
pub enum RibbonArrow {
    /// Plain button without an arrow.
    #[default]
    None,
    /// The whole button opens a menu (`RibbonX` `menu`).
    Menu,
    /// Primary action + separate arrow region (`RibbonX` `splitButton`).
    /// Rendered identically to `Menu`; the split behavior is the caller's.
    Split,
}
/// In-ribbon gallery: a strip of preview cells plus a 3-button spinner column.
#[derive(Debug, Clone)]
#[repr(C)]
pub struct RibbonGallery {
    /// The visible cells.
    pub cells: RibbonGalleryCellVec,
    /// Index of the selected cell.
    pub selected: usize,
    /// Optional callback fired when a cell is clicked (receives cell index).
    pub on_select: OptionRibbonGalleryOnSelect,
}
/// One gallery cell: an arbitrary preview [`Dom`] over a name label.
#[derive(Debug, Clone)]
#[repr(C)]
pub struct RibbonGalleryCell {
    /// Preview content rendered above the label (e.g. styled sample text).
    pub preview: Dom,
    /// Name label rendered under the preview.
    pub label: AzString,
}
impl_option!(RibbonAppButton, OptionRibbonAppButton, copy = false, [Debug, Clone]);
impl_option!(RibbonTab, OptionRibbonTab, copy = false, [Debug, Clone]);
impl_option!(RibbonGroup, OptionRibbonGroup, copy = false, [Debug, Clone]);
impl_option!(RibbonItem, OptionRibbonItem, copy = false, [Debug, Clone]);
impl_option!(RibbonGalleryCell, OptionRibbonGalleryCell, copy = false, [Debug, Clone]);
impl_vec!(RibbonTab, RibbonTabVec, RibbonTabVecDestructor, RibbonTabVecDestructorType, RibbonTabVecSlice, OptionRibbonTab);
impl_vec_clone!(RibbonTab, RibbonTabVec, RibbonTabVecDestructor);
impl_vec_debug!(RibbonTab, RibbonTabVec);
impl_vec_mut!(RibbonTab, RibbonTabVec);
impl_vec!(RibbonGroup, RibbonGroupVec, RibbonGroupVecDestructor, RibbonGroupVecDestructorType, RibbonGroupVecSlice, OptionRibbonGroup);
impl_vec_clone!(RibbonGroup, RibbonGroupVec, RibbonGroupVecDestructor);
impl_vec_debug!(RibbonGroup, RibbonGroupVec);
impl_vec_mut!(RibbonGroup, RibbonGroupVec);
impl_vec!(RibbonItem, RibbonItemVec, RibbonItemVecDestructor, RibbonItemVecDestructorType, RibbonItemVecSlice, OptionRibbonItem);
impl_vec_clone!(RibbonItem, RibbonItemVec, RibbonItemVecDestructor);
impl_vec_debug!(RibbonItem, RibbonItemVec);
impl_vec_mut!(RibbonItem, RibbonItemVec);
impl_vec!(RibbonGalleryCell, RibbonGalleryCellVec, RibbonGalleryCellVecDestructor, RibbonGalleryCellVecDestructorType, RibbonGalleryCellVecSlice, OptionRibbonGalleryCell);
impl_vec_clone!(RibbonGalleryCell, RibbonGalleryCellVec, RibbonGalleryCellVecDestructor);
impl_vec_debug!(RibbonGalleryCell, RibbonGalleryCellVec);
impl_vec_mut!(RibbonGalleryCell, RibbonGalleryCellVec);
// -- Constructors / builders --
impl RibbonAppButton {
    /// Creates an application button with the given label and no callback.
    #[must_use]
123
    pub fn new(label: AzString) -> Self {
123
        Self { label, on_click: None.into() }
123
    }
    /// Sets the click callback.
1
    pub fn set_on_click<C: Into<super::button::ButtonOnClickCallback>>(
1
        &mut self,
1
        data: RefAny,
1
        on_click: C,
1
    ) {
1
        self.on_click = Some(super::button::ButtonOnClick {
1
            refany: data,
1
            callback: on_click.into(),
1
        })
1
        .into();
1
    }
    /// Builder method: sets the click callback and returns `self`.
    #[must_use]
1
    pub fn with_on_click<C: Into<super::button::ButtonOnClickCallback>>(
1
        mut self,
1
        data: RefAny,
1
        on_click: C,
1
    ) -> Self {
1
        self.set_on_click(data, on_click);
1
        self
1
    }
}
impl RibbonTab {
    /// Creates a new tab with the given label and no groups.
    #[must_use]
1029
    pub const fn new(label: AzString) -> Self {
1029
        Self {
1029
            label,
1029
            groups: RibbonGroupVec::from_const_slice(&[]),
1029
            style: CssPropertyWithConditionsVec::from_const_slice(&[]),
1029
        }
1029
    }
    /// Appends per-tab style properties to this tab's header (see
    /// [`RibbonTab::style`]).
    pub fn set_style(&mut self, style: CssPropertyWithConditionsVec) {
        self.style = style;
    }
    /// Builder method: sets the per-tab header style and returns `self`.
    #[must_use]
    pub fn with_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
        self.set_style(style);
        self
    }
    /// Appends a group to this tab.
12151
    pub fn add_group(&mut self, group: RibbonGroup) {
12151
        self.groups.push(group);
12151
    }
    /// Builder method: appends a group and returns `self`.
    #[must_use]
12151
    pub fn with_group(mut self, group: RibbonGroup) -> Self {
12151
        self.add_group(group);
12151
        self
12151
    }
}
impl RibbonGroup {
    /// Creates a new group with the given caption and no items.
    #[must_use]
1375
    pub const fn new(label: AzString) -> Self {
1375
        Self {
1375
            label,
1375
            items: RibbonItemVec::from_const_slice(&[]),
1375
            launcher: OptionButtonOnClick::None,
1375
            fills_space: false,
1375
        }
1375
    }
    /// Builder method: makes this group absorb the remaining ribbon width.
    #[must_use]
120
    pub const fn with_fills_space(mut self, fills_space: bool) -> Self {
120
        self.fills_space = fills_space;
120
        self
120
    }
    /// Appends an item to this group.
12877
    pub fn add_item(&mut self, item: RibbonItem) {
12877
        self.items.push(item);
12877
    }
    /// Builder method: appends an item and returns `self`.
    #[must_use]
12877
    pub fn with_item(mut self, item: RibbonItem) -> Self {
12877
        self.add_item(item);
12877
        self
12877
    }
    /// Sets the dialog-box-launcher callback (renders the launcher button).
2
    pub fn set_launcher<C: Into<super::button::ButtonOnClickCallback>>(
2
        &mut self,
2
        data: RefAny,
2
        on_click: C,
2
    ) {
2
        self.launcher = Some(super::button::ButtonOnClick {
2
            refany: data,
2
            callback: on_click.into(),
2
        })
2
        .into();
2
    }
    /// Builder method: sets the launcher callback and returns `self`.
    #[must_use]
2
    pub fn with_launcher<C: Into<super::button::ButtonOnClickCallback>>(
2
        mut self,
2
        data: RefAny,
2
        on_click: C,
2
    ) -> Self {
2
        self.set_launcher(data, on_click);
2
        self
2
    }
}
impl RibbonColumn {
    /// Creates an empty column.
    #[must_use]
359
    pub const fn new() -> Self {
359
        Self { items: RibbonItemVec::from_const_slice(&[]) }
359
    }
    /// Appends an item to this column.
9634
    pub fn add_item(&mut self, item: RibbonItem) {
9634
        self.items.push(item);
9634
    }
    /// Builder method: appends an item and returns `self`.
    #[must_use]
9634
    pub fn with_item(mut self, item: RibbonItem) -> Self {
9634
        self.add_item(item);
9634
        self
9634
    }
}
impl Default for RibbonColumn {
    fn default() -> Self {
        Self::new()
    }
}
impl RibbonRow {
    /// Creates an empty row.
    #[must_use]
1
    pub const fn new() -> Self {
1
        Self { items: RibbonItemVec::from_const_slice(&[]) }
1
    }
    /// Appends an item to this row.
2
    pub fn add_item(&mut self, item: RibbonItem) {
2
        self.items.push(item);
2
    }
    /// Builder method: appends an item and returns `self`.
    #[must_use]
2
    pub fn with_item(mut self, item: RibbonItem) -> Self {
2
        self.add_item(item);
2
        self
2
    }
}
impl Default for RibbonRow {
    fn default() -> Self {
        Self::new()
    }
}
impl RibbonButton {
    /// Creates a plain button with an icon and a label (both may be empty).
    #[must_use]
2025
    pub const fn new(icon: AzString, label: AzString) -> Self {
2025
        Self {
2025
            icon,
2025
            label,
2025
            arrow: RibbonArrow::None,
2025
            toggled: false,
2025
            on_click: OptionButtonOnClick::None,
2025
        }
2025
    }
    /// Builder method: sets the arrow decoration and returns `self`.
    #[must_use]
2
    pub const fn with_arrow(mut self, arrow: RibbonArrow) -> Self {
2
        self.arrow = arrow;
2
        self
2
    }
    /// Builder method: sets the toggled state and returns `self`.
    #[must_use]
1
    pub const fn with_toggled(mut self, toggled: bool) -> Self {
1
        self.toggled = toggled;
1
        self
1
    }
    /// Sets the click callback.
    pub fn set_on_click<C: Into<super::button::ButtonOnClickCallback>>(
        &mut self,
        data: RefAny,
        on_click: C,
    ) {
        self.on_click = Some(super::button::ButtonOnClick {
            refany: data,
            callback: on_click.into(),
        })
        .into();
    }
    /// Builder method: sets the click callback and returns `self`.
    #[must_use]
    pub fn with_on_click<C: Into<super::button::ButtonOnClickCallback>>(
        mut self,
        data: RefAny,
        on_click: C,
    ) -> Self {
        self.set_on_click(data, on_click);
        self
    }
}
impl RibbonGallery {
    /// Creates a gallery from its cells; cell 0 is selected.
    #[must_use]
138
    pub fn new(cells: RibbonGalleryCellVec) -> Self {
138
        Self { cells, selected: 0, on_select: None.into() }
138
    }
    /// Builder method: sets the selected cell index and returns `self`.
    #[must_use]
1
    pub const fn with_selected(mut self, selected: usize) -> Self {
1
        self.selected = selected;
1
        self
1
    }
    /// Sets the cell-click callback.
1
    pub fn set_on_select<C: Into<RibbonGalleryOnSelectCallback>>(
1
        &mut self,
1
        data: RefAny,
1
        on_select: C,
1
    ) {
1
        self.on_select = Some(RibbonGalleryOnSelect {
1
            refany: data,
1
            callback: on_select.into(),
1
        })
1
        .into();
1
    }
    /// Builder method: sets the cell-click callback and returns `self`.
    #[must_use]
1
    pub fn with_on_select<C: Into<RibbonGalleryOnSelectCallback>>(
1
        mut self,
1
        data: RefAny,
1
        on_select: C,
1
    ) -> Self {
1
        self.set_on_select(data, on_select);
1
        self
1
    }
}
impl RibbonGalleryCell {
    /// Creates a cell from a preview subtree and a name label.
    #[must_use]
572
    pub const fn new(preview: Dom, label: AzString) -> Self {
572
        Self { preview, label }
572
    }
}
impl Ribbon {
    /// Creates a new ribbon with the given tabs, the first tab active and the
    /// the Office-2013-era look default style.
    #[must_use]
1213
    pub fn new(tabs: RibbonTabVec) -> Self {
1213
        Self {
1213
            app_button: None.into(),
1213
            tabs,
1213
            active_tab: 0,
1213
            on_tab_click: None.into(),
1213
            style: RibbonStyle::office_2013(),
1213
            behavior: RibbonBehavior::office_2013(),
1213
        }
1213
    }
    /// Sets the application button ("FILE").
1059
    pub fn set_app_button(&mut self, app_button: RibbonAppButton) {
1059
        self.app_button = Some(app_button).into();
1059
    }
    /// Builder method: sets the application button and returns `self`.
    #[must_use]
1059
    pub fn with_app_button(mut self, app_button: RibbonAppButton) -> Self {
1059
        self.set_app_button(app_button);
1059
        self
1059
    }
    /// Replaces the whole style bundle.
    pub fn set_style(&mut self, style: RibbonStyle) {
        self.style = style;
    }
    /// Builder method: replaces the style bundle and returns `self`.
    #[must_use]
    pub fn with_style(mut self, style: RibbonStyle) -> Self {
        self.set_style(style);
        self
    }
    /// Replaces the self-driven behavior set (collapse, peek, gallery).
7
    pub const fn set_behavior(&mut self, behavior: RibbonBehavior) {
7
        self.behavior = behavior;
7
    }
    /// Builder method: replaces the behavior set and returns `self`.
    #[must_use]
7
    pub const fn with_behavior(mut self, behavior: RibbonBehavior) -> Self {
7
        self.set_behavior(behavior);
7
        self
7
    }
    /// Sets the active tab by index, clamping to the last valid tab.
14
    pub const fn set_active_tab(&mut self, index: usize) {
14
        let max = self.tabs.len().saturating_sub(1);
14
        self.active_tab = if index > max { max } else { index };
14
    }
    /// Builder method: sets the active tab (clamped) and returns `self`.
    #[must_use]
2
    pub const fn with_active_tab(mut self, index: usize) -> Self {
2
        self.set_active_tab(index);
2
        self
2
    }
    /// Registers a callback invoked when a tab is clicked.
1
    pub fn set_on_tab_click<C: Into<RibbonOnTabClickCallback>>(&mut self, data: RefAny, cb: C) {
1
        self.on_tab_click = Some(RibbonOnTabClick {
1
            callback: cb.into(), refany: data,
1
        }).into();
1
    }
    /// Builder method: registers a tab-click callback and returns `self`.
    #[must_use]
1
    pub fn with_on_tab_click<C: Into<RibbonOnTabClickCallback>>(mut self, data: RefAny, cb: C) -> Self {
1
        self.set_on_tab_click(data, cb);
1
        self
1
    }
    /// Builds the ADAPTIVE ribbon DOM: both the desktop chrome (tab strip)
    /// and the touch chrome (full-width tab button, group list) live in the
    /// tree, and inline viewport conditions decide which is visible. Use
    /// this when one tree must serve every window size without re-running
    /// `layout()` logic.
    #[must_use]
160
    pub fn dom(self) -> Dom {
160
        self.build_chrome(RibbonChromeMode::Adaptive)
160
    }
    /// Builds ONLY the desktop chrome (tab strip + content band), with no
    /// mobile nodes and no viewport conditions. Pair with
    /// [`Self::dom_mobile`] by branching on
    /// `LayoutCallbackInfo::viewport_bigger_than` in `layout()` - the
    /// framework re-invokes `layout()` on every resize, so crossing the
    /// breakpoint swaps the structure.
    #[must_use]
1045
    pub fn dom_desktop(self) -> Dom {
1045
        self.build_chrome(RibbonChromeMode::Desktop)
1045
    }
    /// Builds ONLY the touch chrome: the full-width active-tab button (tap
    /// opens the fullscreen tab picker, double-tap collapses the band), the
    /// scrollable group list on the dominant-hand side, and ONE visible
    /// group at a time (tapping a list entry swaps it in with no app
    /// relayout). See [`Self::dom_desktop`] for the pairing contract.
    #[must_use]
1
    pub fn dom_mobile(self) -> Dom {
1
        self.build_chrome(RibbonChromeMode::Mobile)
1
    }
1206
    fn build_chrome(self, mode: RibbonChromeMode) -> Dom {
1206
        let Self { app_button, tabs, active_tab, on_tab_click, style, behavior } = self;
1206
        let has_callback = on_tab_click.is_some();
        // Labels are needed by both chromes; `tabs` is consumed below.
1206
        let tab_labels: Vec<AzString> =
8611
            tabs.as_slice().iter().map(|t| t.label.clone()).collect();
1206
        let group_labels: Vec<AzString> = tabs
1206
            .as_slice()
1206
            .get(active_tab)
4779
            .map(|t| t.groups.as_slice().iter().map(|g| g.label.clone()).collect())
1206
            .unwrap_or_default();
1206
        let mut bar_children: Vec<Dom> = Vec::with_capacity(tabs.len() + 2);
1206
        if let Some(ab) = app_button.into_option() {
1059
            let mut d = Dom::create_div()
1059
                .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_APP_BUTTON))
1059
                .with_css_props(style.app_button_style.clone())
1059
                .with_children(DomVec::from_vec(vec![Dom::create_p_with_text(ab.label)]));
1059
            if let Some(oc) = ab.on_click.into_option() {
1
                d = d.with_callbacks(vec![CoreCallbackData {
1
                    event: EventFilter::Hover(HoverEventFilter::MouseUp),
1
                    callback: CoreCallback {
1
                        cb: oc.callback.cb as *const () as usize,
1
                        ctx: oc.callback.ctx,
1
                    },
1
                    refany: oc.refany,
1
                }].into());
1058
            }
1059
            bar_children.push(d);
147
        }
        // Private chrome state shared by every tab header: the collapse and
        // hover-peek behaviors are driven from here, so the application's own
        // data model never has to model ribbon chrome.
1206
        let chrome = RefAny::new(RibbonChromeState { collapsed: false });
8611
        for (idx, tab) in tabs.as_slice().iter().enumerate() {
8611
            let (classes, part_style) = if idx == active_tab {
1204
                (CLS_TAB_ACTIVE, style.tab_active_style.clone())
            } else {
7407
                (CLS_TAB, style.tab_style.clone())
            };
            // Per-tab properties go AFTER the shared ones so they win, and
            // they are applied in both the active and inactive state.
8611
            let part_style = if tab.style.as_ref().is_empty() {
8611
                part_style
            } else {
                let mut merged = part_style.into_library_owned_vec();
                merged.extend(tab.style.as_ref().iter().cloned());
                CssPropertyWithConditionsVec::from_vec(merged)
            };
8611
            let mut d = Dom::create_div()
8611
                .with_ids_and_classes(IdOrClassVec::from_const_slice(classes))
8611
                .with_css_props(part_style)
8611
                .with_children(DomVec::from_vec(vec![Dom::create_p_with_text(tab.label.clone())]));
8611
            let mut cbs: Vec<CoreCallbackData> = Vec::with_capacity(4);
8611
            if has_callback {
5
                cbs.push(CoreCallbackData {
5
                    event: EventFilter::Hover(HoverEventFilter::MouseUp),
5
                    callback: CoreCallback {
5
                        cb: on_ribbon_tab_click as usize,
5
                        ctx: azul_core::refany::OptionRefAny::None,
5
                    },
5
                    refany: RefAny::new(TabClickData {
5
                        tab_idx: idx, on_tab_click: on_tab_click.clone(),
5
                    }),
5
                });
8606
            }
8611
            if behavior.collapsible {
8600
                cbs.push(CoreCallbackData {
8600
                    event: EventFilter::Hover(HoverEventFilter::DoubleClick),
8600
                    callback: CoreCallback {
8600
                        cb: on_ribbon_tab_double_click as usize,
8600
                        ctx: azul_core::refany::OptionRefAny::None,
8600
                    },
8600
                    refany: chrome.clone(),
8600
                });
8600
                if behavior.peek_on_hover {
8599
                    cbs.push(CoreCallbackData {
8599
                        event: EventFilter::Hover(HoverEventFilter::MouseEnter),
8599
                        callback: CoreCallback {
8599
                            cb: on_ribbon_tab_peek_enter as usize,
8599
                            ctx: azul_core::refany::OptionRefAny::None,
8599
                        },
8599
                        refany: chrome.clone(),
8599
                    });
8599
                    cbs.push(CoreCallbackData {
8599
                        event: EventFilter::Hover(HoverEventFilter::MouseLeave),
8599
                        callback: CoreCallback {
8599
                            cb: on_ribbon_tab_peek_leave as usize,
8599
                            ctx: azul_core::refany::OptionRefAny::None,
8599
                        },
8599
                        refany: chrome.clone(),
8599
                    });
8599
                }
11
            }
8611
            if !cbs.is_empty() {
8600
                d = d.with_callbacks(cbs.into());
8600
            }
8611
            bar_children.push(d);
        }
1206
        bar_children.push(
1206
            Dom::create_div()
1206
                .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_TAB_FILLER))
1206
                .with_css_props(style.tab_filler_style.clone()),
        );
1206
        let tab_bar = Dom::create_div()
1206
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_TAB_BAR))
1206
            .with_css_props(style.tab_bar_style.clone())
1206
            .with_children(DomVec::from_vec(bar_children));
1206
        let mut group_doms: Vec<Dom> = match tabs.into_library_owned_vec().into_iter().nth(active_tab) {
1204
            Some(active) => active
1204
                .groups
1204
                .into_library_owned_vec()
1204
                .into_iter()
4777
                .map(|g| group_dom(g, &style, behavior))
1204
                .collect(),
2
            None => Vec::new(),
        };
        // Structural mobile chrome shows ONE group at a time; the group list
        // beside the content swaps them in (a runtime display patch - no app
        // relayout, same mechanism as the gallery panel).
1206
        if matches!(mode, RibbonChromeMode::Mobile) {
3
            for (idx, g) in group_doms.iter_mut().enumerate() {
3
                if idx != 0 {
2
                    g.root.upsert_inline_css_property(P::const_display(LayoutDisplay::None));
2
                }
            }
1205
        }
        // ---- mobile chrome -------------------------------------------
        // Same tabs and groups, touch presentation. Both chromes live in the
        // tree and the viewport condition decides which is visible, so there
        // is no second widget tree and no state to keep in sync.
1206
        let active_label = tab_labels
1206
            .get(active_tab)
1206
            .cloned()
1206
            .unwrap_or_else(|| AzString::from_const_str(""));
1206
        let mut mobile_tab_button = Dom::create_div()
1206
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_MOBILE_TAB_BUTTON))
1206
            .with_css_props(style.mobile_tab_button_style.clone())
1206
            .with_children(DomVec::from_vec(vec![
1206
                Dom::create_p()
1206
                    .with_css_props(style.mobile_tab_label_style.clone())
1206
                    .with_children(DomVec::from_vec(vec![Dom::create_text_do_not_use_without_block_level_wrapper(active_label)])),
1206
                Dom::create_icon(AzString::from_const_str("expand_more"))
1206
                    .with_css_props(style.mobile_tab_arrow_style.clone()),
            ]));
1206
        let mut mobile_cbs: Vec<CoreCallbackData> = Vec::with_capacity(2);
1206
        if behavior.mobile_tab_overlay {
1201
            mobile_cbs.push(CoreCallbackData {
1201
                event: EventFilter::Hover(HoverEventFilter::MouseUp),
1201
                callback: CoreCallback {
1201
                    cb: on_ribbon_mobile_tab_click as usize,
1201
                    ctx: azul_core::refany::OptionRefAny::None,
1201
                },
1201
                refany: RefAny::new(MobileTabData { open: false }),
1201
            });
1201
        }
        // Double tap collapses the band, exactly like a desktop double click.
1206
        if behavior.collapsible {
1201
            mobile_cbs.push(CoreCallbackData {
1201
                event: EventFilter::Hover(HoverEventFilter::DoubleClick),
1201
                callback: CoreCallback {
1201
                    cb: on_ribbon_tab_double_click as usize,
1201
                    ctx: azul_core::refany::OptionRefAny::None,
1201
                },
1201
                refany: chrome.clone(),
1201
            });
1201
        }
1206
        if !mobile_cbs.is_empty() {
1201
            mobile_tab_button = mobile_tab_button.with_callbacks(mobile_cbs.into());
1201
        }
        // Full-screen tab picker, hidden until the button opens it.
1206
        let overlay_items: Vec<Dom> = tab_labels
1206
            .iter()
1206
            .enumerate()
8611
            .map(|(idx, label)| {
8611
                let mut item = Dom::create_div()
8611
                    .with_ids_and_classes(IdOrClassVec::from_const_slice(
8611
                        CLS_MOBILE_TAB_OVERLAY_ITEM,
                    ))
8611
                    .with_css_props(style.mobile_tab_overlay_item_style.clone())
8611
                    .with_children(DomVec::from_vec(vec![Dom::create_p_with_text(label.clone())]));
8611
                if has_callback {
5
                    item = item.with_callbacks(vec![CoreCallbackData {
5
                        event: EventFilter::Hover(HoverEventFilter::MouseUp),
5
                        callback: CoreCallback {
5
                            cb: on_ribbon_tab_click as usize,
5
                            ctx: azul_core::refany::OptionRefAny::None,
5
                        },
5
                        refany: RefAny::new(TabClickData {
5
                            tab_idx: idx,
5
                            on_tab_click: on_tab_click.clone(),
5
                        }),
5
                    }].into());
8606
                }
8611
                item
8611
            })
1206
            .collect();
1206
        let mobile_tab_overlay = Dom::create_div()
1206
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_MOBILE_TAB_OVERLAY))
1206
            .with_css_props(style.mobile_tab_overlay_style.clone())
1206
            .with_children(DomVec::from_vec(overlay_items));
        // Group list: on phones ONE group is visible and the rest are a
        // scrollable list on the dominant-hand side.
1206
        let group_list_items: Vec<Dom> = group_labels
1206
            .iter()
1206
            .enumerate()
4779
            .map(|(idx, label)| {
4769
                let item_style = if idx == 0 {
1192
                    merged_style(
1192
                        &style.mobile_group_list_item_style,
1192
                        &style.mobile_group_list_item_selected_style,
                    )
                } else {
3577
                    style.mobile_group_list_item_style.clone()
                };
4769
                let mut item = Dom::create_div()
4769
                    .with_ids_and_classes(IdOrClassVec::from_const_slice(
4769
                        CLS_MOBILE_GROUP_LIST_ITEM,
                    ))
4769
                    .with_css_props(item_style)
4769
                    .with_children(DomVec::from_vec(vec![Dom::create_p_with_text(label.clone())]));
4769
                if matches!(mode, RibbonChromeMode::Mobile) {
3
                    item = item.with_callbacks(vec![CoreCallbackData {
3
                        event: EventFilter::Hover(HoverEventFilter::MouseUp),
3
                        callback: CoreCallback {
3
                            cb: on_ribbon_mobile_group_click as usize,
3
                            ctx: azul_core::refany::OptionRefAny::None,
3
                        },
3
                        refany: RefAny::new(GroupListClickData {
3
                            group_idx: idx,
3
                            selected_style: style.mobile_group_list_item_selected_style.clone(),
3
                            base_style: style.mobile_group_list_item_style.clone(),
3
                        }),
3
                    }].into());
4766
                }
4769
                item
4769
            })
1206
            .collect();
1206
        let mobile_group_list = Dom::create_div()
1206
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_MOBILE_GROUP_LIST))
1206
            .with_css_props(style.mobile_group_list_style.clone())
1206
            .with_children(DomVec::from_vec(group_list_items));
1206
        let content = Dom::create_div()
1206
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_CONTENT))
1206
            .with_css_props(style.content_style.clone())
1206
            .with_children(DomVec::from_vec(group_doms));
        // Structural modes pin the chrome's visibility unconditionally
        // (inline resolution is last-wins, so the appended value overrides
        // the baked viewport condition): the STRUCTURE is the breakpoint
        // switch, not the stylesheet.
1206
        let children = match mode {
160
            RibbonChromeMode::Adaptive => vec![
160
                tab_bar,
160
                mobile_tab_button,
160
                mobile_tab_overlay,
160
                mobile_group_list,
160
                content,
            ],
            RibbonChromeMode::Desktop => {
1045
                let mut tab_bar = tab_bar;
1045
                tab_bar.root.upsert_inline_css_property(P::const_display(LayoutDisplay::Flex));
1045
                vec![tab_bar, content]
            }
            RibbonChromeMode::Mobile => {
1
                let mut mobile_tab_button = mobile_tab_button;
1
                let mut mobile_group_list = mobile_group_list;
1
                mobile_tab_button
1
                    .root
1
                    .upsert_inline_css_property(P::const_display(LayoutDisplay::Flex));
1
                mobile_group_list
1
                    .root
1
                    .upsert_inline_css_property(P::const_display(LayoutDisplay::Flex));
                // The group list and the ONE visible group share a ROW band
                // ("scrollable list beside the content", the touch spec) -
                // structurally, because the container is a column. List side
                // = leading (right-handed default); a Handedness-driven
                // row-reverse via a future mobile_band_style field can flip
                // it without changing this structure.
1
                let band = Dom::create_div()
1
                    .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_MOBILE_BAND))
1
                    .with_css_props(CssPropertyWithConditionsVec::from_vec(vec![
1
                        Cond::simple(P::const_display(LayoutDisplay::Flex)),
1
                        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
1
                        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
1
                        cond_border_box(),
                    ]))
1
                    .with_children(DomVec::from_vec(vec![mobile_group_list, content]));
1
                vec![mobile_tab_button, mobile_tab_overlay, band]
            }
        };
1206
        let mut container = Dom::create_div()
1206
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_RIBBON))
1206
            .with_css_props(style.container_style)
1206
            .with_children(DomVec::from_vec(children));
        // The chrome state (collapse flag) lives on the container as a
        // DATASET so it follows node identity across RefreshDom rebuilds
        // (see keep_old_ribbon_chrome). Without this, every rebuild reset
        // collapsed=false: double-clicking a tab collapsed the band and the
        // tab-click's own RefreshDom immediately forgot it. Only attached
        // when a chrome behavior is active - an inert ribbon has no chrome
        // state to persist (and `Dom` equality stays meaningful for it).
1206
        if behavior.collapsible || behavior.peek_on_hover {
1201
            container.root.set_dataset(azul_core::refany::OptionRefAny::Some(chrome));
1201
            // The `as` is NOT trivial: it coerces the fn ITEM to a fn POINTER,
1201
            // which is what `DatasetMergeCallback: From<...>` is implemented for.
1201
            // Dropping it fails to satisfy the bound.
1201
            #[allow(trivial_casts)]
1201
            container.root.set_merge_callback(
1201
                keep_old_ribbon_chrome as azul_core::dom::DatasetMergeCallbackType,
1201
            );
1201
        }
1206
        container
1206
    }
}
// -- DOM assembly helpers --
/// `base` with `extra` appended (inline CSS resolves last-wins, so `extra`
/// overrides `base` where they collide).
4717
fn merged_style(
4717
    base: &CssPropertyWithConditionsVec,
4717
    extra: &CssPropertyWithConditionsVec,
4717
) -> CssPropertyWithConditionsVec {
4717
    if extra.as_ref().is_empty() {
        return base.clone();
4717
    }
4717
    let mut v: Vec<Cond> = base.as_ref().to_vec();
4717
    v.extend_from_slice(extra.as_ref());
4717
    CssPropertyWithConditionsVec::from_vec(v)
4717
}
/// Expands ribbon button config to the existing [`Button`] widget with the
/// given part styles injected through `Button`'s public style fields.
14268
fn styled_button(
14268
    icon: AzString,
14268
    label: AzString,
14268
    trailing_icon: AzString,
14268
    container_style: CssPropertyWithConditionsVec,
14268
    icon_style: CssPropertyWithConditionsVec,
14268
    label_style: CssPropertyWithConditionsVec,
14268
    trailing_icon_style: CssPropertyWithConditionsVec,
14268
    on_click: OptionButtonOnClick,
14268
) -> Dom {
14268
    let mut b = Button::create(label);
14268
    b.icon = icon;
14268
    b.trailing_icon = trailing_icon;
14268
    b.container_style = container_style;
14268
    b.icon_style = icon_style;
14268
    b.label_style = label_style;
14268
    b.trailing_icon_style = trailing_icon_style;
14268
    b.on_click = on_click;
14268
    b.dom()
14268
}
10732
fn expand_ribbon_button(rb: RibbonButton, large: bool, s: &RibbonStyle) -> Dom {
10732
    let base = if large { &s.large_button_style } else { &s.small_button_style };
10732
    let container = if rb.toggled {
1
        merged_style(base, &s.checked_style)
    } else {
10731
        base.clone()
    };
10732
    let trailing = match rb.arrow {
10730
        RibbonArrow::None => AzString::from_const_str(""),
2
        RibbonArrow::Menu | RibbonArrow::Split => AzString::from_const_str("arrow_drop_down"),
    };
10732
    let (icon_style, label_style) = if large {
1094
        (s.large_icon_style.clone(), s.large_label_style.clone())
    } else {
9638
        (s.small_icon_style.clone(), s.small_label_style.clone())
    };
10732
    styled_button(
10732
        rb.icon,
10732
        rb.label,
10732
        trailing,
10732
        container,
10732
        icon_style,
10732
        label_style,
10732
        s.arrow_icon_style.clone(),
10732
        rb.on_click,
    )
10732
}
15131
fn item_dom(item: RibbonItem, s: &RibbonStyle, b: RibbonBehavior) -> Dom {
15131
    match item {
1094
        RibbonItem::LargeButton(rb) => expand_ribbon_button(rb, true, s),
9638
        RibbonItem::SmallButton(rb) => expand_ribbon_button(rb, false, s),
3215
        RibbonItem::Column(col) => Dom::create_div()
3215
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_COLUMN))
3215
            .with_css_props(s.column_style.clone())
3215
            .with_children(DomVec::from_vec(
3215
                col.items
3215
                    .into_library_owned_vec()
3215
                    .into_iter()
9634
                    .map(|it| item_dom(it, s, b))
3215
                    .collect(),
            )),
1
        RibbonItem::Row(row) => Dom::create_div()
1
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_ROW))
1
            .with_css_props(s.row_style.clone())
1
            .with_children(DomVec::from_vec(
1
                row.items
1
                    .into_library_owned_vec()
1
                    .into_iter()
2
                    .map(|it| item_dom(it, s, b))
1
                    .collect(),
            )),
1
        RibbonItem::Combo(combo) => combo.dom(),
1
        RibbonItem::Drop(drop) => drop.dom(),
1
        RibbonItem::Check(check) => check.dom(),
1178
        RibbonItem::Gallery(gallery) => gallery_dom(gallery, s, b),
1
        RibbonItem::Separator => Dom::create_div()
1
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_SEPARATOR))
1
            .with_css_props(s.separator_style.clone()),
1
        RibbonItem::Custom(dom) => dom,
    }
15131
}
/// Appended to the group style when [`RibbonGroup::fills_space`] is set:
/// the group absorbs leftover width AND yields it under pressure, down to
/// an explicit floor. The explicit `min-width` is load-bearing — it
/// replaces the flex automatic minimum size, which taffy 0.10 does not
/// collapse across the nested group > items > gallery-frame chain (see
/// `layout/tests/flex_intrinsic_text.rs`).
static GROUP_FILL_STYLE: &[Cond] = &[
    Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
    Cond::simple(P::const_flex_shrink(LayoutFlexShrink {
        inner: FloatValue::const_new(1),
    })),
    Cond::simple(P::const_min_width(LayoutMinWidth::const_px(160))),
];
4769
fn group_dom(group: RibbonGroup, s: &RibbonStyle, b: RibbonBehavior) -> Dom {
4769
    let RibbonGroup { label, items, launcher, fills_space } = group;
4769
    let item_doms: Vec<Dom> = items
4769
        .into_library_owned_vec()
4769
        .into_iter()
5498
        .map(|it| item_dom(it, s, b))
4769
        .collect();
4769
    let items_row = Dom::create_div()
4769
        .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_GROUP_ITEMS))
4769
        .with_css_props(s.group_items_style.clone())
4769
        .with_children(DomVec::from_vec(item_doms));
4769
    let has_launcher = launcher.is_some();
4769
    let mut footer_children: Vec<Dom> = Vec::with_capacity(3);
4769
    if has_launcher {
2
        // Balances the launcher's width so the caption stays centered.
2
        footer_children.push(
2
            Dom::create_div()
2
                .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_FOOTER_SPACER))
2
                .with_css_props(s.footer_spacer_style.clone()),
2
        );
4767
    }
4769
    footer_children.push(
4769
        Dom::create_p()
4769
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_GROUP_LABEL))
4769
            .with_css_props(s.group_label_style.clone())
4769
            .with_children(DomVec::from_vec(vec![Dom::create_text_do_not_use_without_block_level_wrapper(label)])),
    );
4769
    if let Some(l) = launcher.into_option() {
2
        footer_children.push(styled_button(
2
            AzString::from_const_str("south_east"),
2
            AzString::from_const_str(""),
2
            AzString::from_const_str(""),
2
            s.launcher_button_style.clone(),
2
            s.launcher_icon_style.clone(),
2
            s.small_label_style.clone(),
2
            s.arrow_icon_style.clone(),
2
            Some(l).into(),
2
        ));
4767
    }
4769
    let footer = Dom::create_div()
4769
        .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_GROUP_FOOTER))
4769
        .with_css_props(s.group_footer_style.clone())
4769
        .with_children(DomVec::from_vec(footer_children));
4769
    let group_style = if fills_space {
1170
        merged_style(
1170
            &s.group_style,
1170
            &CssPropertyWithConditionsVec::from_const_slice(GROUP_FILL_STYLE),
        )
    } else {
3599
        s.group_style.clone()
    };
4769
    Dom::create_div()
4769
        .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_GROUP))
4769
        .with_css_props(group_style)
4769
        .with_children(DomVec::from_vec(vec![items_row, footer]))
4769
}
1178
fn gallery_dom(gallery: RibbonGallery, s: &RibbonStyle, b: RibbonBehavior) -> Dom {
1178
    let RibbonGallery { cells, selected, on_select } = gallery;
1178
    let has_callback = on_select.is_some();
1178
    let cells = cells.into_library_owned_vec();
    // The cells are built twice: once for the in-ribbon strip and once for
    // the expansion panel, so "More" can show every cell without a relayout.
2354
    let build_cells = |in_panel: bool| -> Vec<Dom> {
2354
        let mut out: Vec<Dom> = Vec::with_capacity(cells.len());
9940
        for (idx, cell) in cells.iter().enumerate() {
9940
            let (classes, cell_style) = if idx == selected {
2354
                (
2354
                    CLS_GALLERY_CELL_SELECTED,
2354
                    merged_style(&s.gallery_cell_style, &s.gallery_cell_selected_style),
2354
                )
            } else {
7586
                (CLS_GALLERY_CELL, s.gallery_cell_style.clone())
            };
9940
            let label = Dom::create_p()
9940
                .with_css_props(s.gallery_cell_label_style.clone())
9940
                .with_children(DomVec::from_vec(vec![Dom::create_text_do_not_use_without_block_level_wrapper(cell.label.clone())]));
9940
            let mut d = Dom::create_div()
9940
                .with_ids_and_classes(IdOrClassVec::from_const_slice(classes))
9940
                .with_css_props(cell_style)
9940
                .with_children(DomVec::from_vec(vec![cell.preview.clone(), label]));
9940
            if has_callback || b.auto_select_gallery {
9938
                d = d.with_callbacks(vec![CoreCallbackData {
9938
                    event: EventFilter::Hover(HoverEventFilter::MouseUp),
9938
                    callback: CoreCallback {
9938
                        cb: on_ribbon_gallery_cell_click as usize,
9938
                        ctx: azul_core::refany::OptionRefAny::None,
9938
                    },
9938
                    refany: RefAny::new(GalleryCellClickData {
9938
                        cell_idx: idx,
9938
                        on_select: on_select.clone(),
9938
                        auto_select: b.auto_select_gallery,
9938
                        in_panel,
9938
                        selected_style: s.gallery_cell_selected_style.clone(),
9938
                        base_style: s.gallery_cell_style.clone(),
9938
                    }),
9938
                }].into());
9938
            }
9940
            out.push(d);
        }
2354
        out
2354
    };
1178
    let strip = Dom::create_div()
1178
        .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_GALLERY_STRIP))
1178
        .with_css_props(s.gallery_strip_style.clone())
1178
        .with_children(DomVec::from_vec(build_cells(false)));
    // Spinner column: scroll-up, scroll-down, and the "More" button that
    // toggles the expansion panel (the classic office-suite "More" chevron-over-bar).
1178
    let spinner_icons = ["expand_less", "expand_more", "arrow_drop_down"];
1178
    let spinner_buttons: Vec<Dom> = spinner_icons
1178
        .iter()
1178
        .enumerate()
3534
        .map(|(i, icon)| {
3534
            let mut btn = styled_button(
3534
                AzString::from(*icon),
3534
                AzString::from_const_str(""),
3534
                AzString::from_const_str(""),
3534
                s.gallery_spinner_button_style.clone(),
3534
                s.gallery_spinner_icon_style.clone(),
3534
                s.small_label_style.clone(),
3534
                s.arrow_icon_style.clone(),
3534
                OptionButtonOnClick::None,
            );
            // The third button is "More": it expands the panel.
3534
            if i == 2 && b.expandable_gallery {
1176
                btn = btn
1176
                    .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_GALLERY_MORE));
1176
                btn = btn.with_callbacks(vec![CoreCallbackData {
1176
                    event: EventFilter::Hover(HoverEventFilter::MouseUp),
1176
                    callback: CoreCallback {
1176
                        cb: on_ribbon_gallery_more_click as usize,
1176
                        ctx: azul_core::refany::OptionRefAny::None,
1176
                    },
1176
                    refany: RefAny::new(GalleryMoreData { open: false }),
1176
                }].into());
2358
            }
3534
            btn
3534
        })
1178
        .collect();
1178
    let spinner = Dom::create_div()
1178
        .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_GALLERY_SPINNER))
1178
        .with_css_props(s.gallery_spinner_style.clone())
1178
        .with_children(DomVec::from_vec(spinner_buttons));
1178
    let frame = Dom::create_div()
1178
        .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_GALLERY))
1178
        .with_css_props(s.gallery_frame_style.clone())
1178
        .with_children(DomVec::from_vec(vec![strip, spinner]));
1178
    if !b.expandable_gallery {
2
        return frame;
1176
    }
    // The expansion panel is an absolutely-positioned wrapped grid of every
    // cell, hidden until "More" is clicked (the popover/combobox pattern).
1176
    let panel = Dom::create_div()
1176
        .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_GALLERY_PANEL))
1176
        .with_css_props(s.gallery_panel_style.clone())
1176
        .with_children(DomVec::from_vec(build_cells(true)));
1176
    Dom::create_div()
1176
        .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_GALLERY_WRAPPER))
1176
        .with_css_props(s.gallery_wrapper_style.clone())
1176
        .with_children(DomVec::from_vec(vec![frame, panel]))
1178
}
// -- Trampolines --
/// Which chrome [`Ribbon::dom`]-family builder emits.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RibbonChromeMode {
    /// Both chromes in one tree; inline viewport conditions pick one.
    Adaptive,
    /// Desktop chrome only (tab strip + content).
    Desktop,
    /// Touch chrome only (tab button + overlay + group list + one group).
    Mobile,
}
/// Per-list-entry payload for the mobile group switcher.
struct GroupListClickData {
    group_idx: usize,
    selected_style: CssPropertyWithConditionsVec,
    base_style: CssPropertyWithConditionsVec,
}
/// Tapping a group-list entry shows THAT group in the content band and moves
/// the highlight - a runtime display patch, no app relayout (the same
/// chokepoint mechanism the gallery panel uses). Targets resolve BY CLASS
/// from the ribbon container, per the widget convention.
extern "C" fn on_ribbon_mobile_group_click(mut refany: RefAny, mut info: CallbackInfo) -> Update {
    let hit = info.get_hit_node();
    let Some(mut data) = refany.downcast_mut::<GroupListClickData>() else {
        return Update::DoNothing;
    };
    let group_idx = data.group_idx;
    let selected_style = data.selected_style.clone();
    let base_style = data.base_style.clone();
    drop(data);
    let Some(ribbon) = ancestor_with_class(&info, hit, RIBBON_CONTAINER_CLASS) else {
        return Update::DoNothing;
    };
    // Swap the visible group inside the content band (the content sits
    // INSIDE the mobile band wrapper, so resolve by descendant search).
    if let Some(content) = descendant_with_class(&info, ribbon, RIBBON_CONTENT_CLASS) {
        let mut group = info.get_first_child(content);
        let mut idx = 0_usize;
        while let Some(g) = group {
            let display = if idx == group_idx {
                LayoutDisplay::Flex
            } else {
                LayoutDisplay::None
            };
            info.set_css_property(g, P::const_display(display));
            group = info.get_next_sibling(g);
            idx += 1;
        }
    }
    // Move the highlight along the list (unconditional props only, like the
    // gallery cell highlight). For a DE-selected entry, property types the
    // selected style sets but the base style does not are reset to Initial -
    // set_css_property(Initial) REMOVES the runtime override, so the entry
    // falls back to its inline style instead of keeping the stale highlight.
    let item = ancestor_with_class(&info, hit, MOBILE_GROUP_LIST_ITEM_CLASS).unwrap_or(hit);
    if let Some(list) = info.get_parent(item) {
        let mut sibling = info.get_first_child(list);
        while let Some(entry) = sibling {
            if entry == item {
                for prop in selected_style.as_ref() {
                    if prop.apply_if.as_ref().is_empty() {
                        info.set_css_property(entry, prop.property.clone());
                    }
                }
            } else {
                for prop in base_style.as_ref() {
                    if prop.apply_if.as_ref().is_empty() {
                        info.set_css_property(entry, prop.property.clone());
                    }
                }
                for prop in selected_style.as_ref() {
                    if !prop.apply_if.as_ref().is_empty() {
                        continue;
                    }
                    let ty = prop.property.get_type();
                    let in_base = base_style.as_ref().iter().any(|b| {
                        b.apply_if.as_ref().is_empty() && b.property.get_type() == ty
                    });
                    if !in_base {
                        info.set_css_property(entry, props::property::CssProperty::initial(ty));
                    }
                }
            }
            sibling = info.get_next_sibling(entry);
        }
    }
    Update::DoNothing
}
/// Dataset merge for the ribbon container: chrome state (collapse) must
/// survive app-driven rebuilds (any callback returning `RefreshDom` - the
/// ribbon's own tab switch does), so keep the OLD allocation wholesale.
/// `diff::transfer_states` then re-points every tab callback refany (they
/// are clones of this dataset) onto the kept allocation, so the handlers
/// keep reading the persistent state with no further wiring.
extern "C" fn keep_old_ribbon_chrome(_new: RefAny, old: RefAny) -> RefAny {
    old
}
struct TabClickData {
    tab_idx: usize,
    on_tab_click: OptionRibbonOnTabClick,
}
4
extern "C" fn on_ribbon_tab_click(mut refany: RefAny, info: CallbackInfo) -> Update {
4
    let Some(mut data) = refany.downcast_mut::<TabClickData>() else {
1
        return Update::DoNothing;
    };
3
    let idx = data.tab_idx;
3
    match data.on_tab_click.as_mut() {
3
        Some(RibbonOnTabClick { refany, callback }) => {
3
            (callback.cb)(refany.clone(), info, idx)
        }
        None => Update::DoNothing,
    }
4
}
/// Private chrome state for the collapse / hover-peek behaviors. Lives in a
/// `RefAny` minted inside [`Ribbon::dom`] and shared by every tab header, so
/// the ribbon can drive its own chrome without any application state.
struct RibbonChromeState {
    collapsed: bool,
}
/// The content band is the tab bar's next sibling; from a tab header that is
/// `parent(tab) -> next_sibling`.
/// The content band of the ribbon that owns `hit`'s tab header, resolved BY
/// CLASS: walk up to the ribbon container, then scan its children for the
/// content class. Positional navigation (`next_sibling(parent(tab))`) broke
/// the moment the container grew more children — the mobile chrome sits
/// between the tab bar and the content band, so a double-click "collapsed"
/// the (already hidden) mobile tab button while the content stayed visible.
fn content_node_of_tab(info: &CallbackInfo, hit: DomNodeId) -> Option<DomNodeId> {
    let tab = ancestor_with_class(info, hit, RIBBON_TAB_CLASS)
        .or_else(|| ancestor_with_class(info, hit, MOBILE_TAB_BUTTON_CLASS))?;
    let ribbon = ancestor_with_class(info, tab, RIBBON_CONTAINER_CLASS)?;
    descendant_with_class(info, ribbon, RIBBON_CONTENT_CLASS)
}
fn set_content_visible(info: &mut CallbackInfo, content: DomNodeId, visible: bool) {
    let display = if visible {
        LayoutDisplay::Flex
    } else {
        LayoutDisplay::None
    };
    info.set_css_property(content, P::const_display(display));
}
/// Double-click on a tab header toggles the collapsed state of the content
/// band (the classic office-suite "Collapse the Ribbon").
extern "C" fn on_ribbon_tab_double_click(mut refany: RefAny, mut info: CallbackInfo) -> Update {
    let hit = info.get_hit_node();
    let Some(content) = content_node_of_tab(&info, hit) else {
        return Update::DoNothing;
    };
    let Some(mut state) = refany.downcast_mut::<RibbonChromeState>() else {
        return Update::DoNothing;
    };
    state.collapsed = !state.collapsed;
    let collapsed = state.collapsed;
    drop(state);
    set_content_visible(&mut info, content, !collapsed);
    Update::DoNothing
}
/// While collapsed, hovering a tab header peeks the content band.
extern "C" fn on_ribbon_tab_peek_enter(mut refany: RefAny, mut info: CallbackInfo) -> Update {
    let hit = info.get_hit_node();
    let Some(content) = content_node_of_tab(&info, hit) else {
        return Update::DoNothing;
    };
    let Some(state) = refany.downcast_ref::<RibbonChromeState>() else {
        return Update::DoNothing;
    };
    let collapsed = state.collapsed;
    drop(state);
    if collapsed {
        set_content_visible(&mut info, content, true);
    }
    Update::DoNothing
}
/// Leaving the tab header hides the peeked band again.
extern "C" fn on_ribbon_tab_peek_leave(mut refany: RefAny, mut info: CallbackInfo) -> Update {
    let hit = info.get_hit_node();
    let Some(content) = content_node_of_tab(&info, hit) else {
        return Update::DoNothing;
    };
    let Some(state) = refany.downcast_ref::<RibbonChromeState>() else {
        return Update::DoNothing;
    };
    let collapsed = state.collapsed;
    drop(state);
    if collapsed {
        set_content_visible(&mut info, content, false);
    }
    Update::DoNothing
}
/// Per-mobile-tab-button state: whether the tab overlay is open.
struct MobileTabData {
    open: bool,
}
/// The mobile tab button opens/closes the full-screen tab picker, which is
/// its next sibling in the ribbon container.
extern "C" fn on_ribbon_mobile_tab_click(mut refany: RefAny, mut info: CallbackInfo) -> Update {
    let hit = info.get_hit_node();
    let Some(button) = ancestor_with_class(&info, hit, MOBILE_TAB_BUTTON_CLASS) else {
        return Update::DoNothing;
    };
    let Some(overlay) = info.get_next_sibling(button) else {
        return Update::DoNothing;
    };
    let Some(mut data) = refany.downcast_mut::<MobileTabData>() else {
        return Update::DoNothing;
    };
    data.open = !data.open;
    let open = data.open;
    drop(data);
    info.set_css_property(
        overlay,
        P::const_display(if open { LayoutDisplay::Flex } else { LayoutDisplay::None }),
    );
    Update::DoNothing
}
/// Per-"More"-button state: whether the expansion panel is open.
struct GalleryMoreData {
    open: bool,
}
/// Walks up from `start` (inclusive) to the first ancestor carrying `class`.
///
/// Hit nodes are not stable: a click on a button can report the button or
/// the icon/label node inside it, and widgets may gain wrapper levels. So
/// the ribbon's handlers locate their targets by CLASS rather than by
/// counting `get_parent` hops - the same identifiers the public CSS API is
/// built on. The walk is bounded so a malformed tree cannot spin.
2
fn ancestor_with_class(
2
    info: &CallbackInfo,
2
    start: DomNodeId,
2
    class: &str,
2
) -> Option<DomNodeId> {
2
    let mut current = Some(start);
4
    for _ in 0..16 {
4
        let node = current?;
2
        if info
2
            .get_node_classes(node)
2
            .as_ref()
2
            .iter()
2
            .any(|c| c.as_str() == class)
        {
            return Some(node);
2
        }
2
        current = info.get_parent(node);
    }
    None
2
}
/// Breadth-first search for the first descendant of `root` carrying `class`,
/// bounded to 64 visited nodes. The ribbon's structural chromes nest parts
/// at different depths (the mobile band wraps group list + content), so
/// class resolution must not assume direct children.
fn descendant_with_class(
    info: &CallbackInfo,
    root: DomNodeId,
    class: &str,
) -> Option<DomNodeId> {
    let mut queue: Vec<DomNodeId> = Vec::with_capacity(8);
    let mut child = info.get_first_child(root);
    while let Some(n) = child {
        queue.push(n);
        child = info.get_next_sibling(n);
    }
    let mut visited = 0_usize;
    let mut i = 0_usize;
    while i < queue.len() && visited < 64 {
        let node = queue[i];
        i += 1;
        visited += 1;
        if info
            .get_node_classes(node)
            .as_ref()
            .iter()
            .any(|cl| cl.as_str() == class)
        {
            return Some(node);
        }
        let mut child = info.get_first_child(node);
        while let Some(n) = child {
            queue.push(n);
            child = info.get_next_sibling(n);
        }
    }
    None
}
/// The "More" button toggles the expansion panel (the gallery wrapper's
/// last child).
extern "C" fn on_ribbon_gallery_more_click(mut refany: RefAny, mut info: CallbackInfo) -> Update {
    let hit = info.get_hit_node();
    let Some(wrapper) = ancestor_with_class(&info, hit, GALLERY_WRAPPER_CLASS) else {
        return Update::DoNothing;
    };
    let Some(panel) = info.get_last_child(wrapper) else {
        return Update::DoNothing;
    };
    let Some(mut data) = refany.downcast_mut::<GalleryMoreData>() else {
        return Update::DoNothing;
    };
    data.open = !data.open;
    let open = data.open;
    drop(data);
    let display = if open {
        LayoutDisplay::Flex
    } else {
        LayoutDisplay::None
    };
    info.set_css_property(panel, P::const_display(display));
    Update::DoNothing
}
struct GalleryCellClickData {
    cell_idx: usize,
    on_select: OptionRibbonGalleryOnSelect,
    /// Move the selection highlight without an app relayout.
    auto_select: bool,
    /// Cells in the expansion panel also close the panel when picked.
    in_panel: bool,
    selected_style: CssPropertyWithConditionsVec,
    base_style: CssPropertyWithConditionsVec,
}
2
extern "C" fn on_ribbon_gallery_cell_click(mut refany: RefAny, mut info: CallbackInfo) -> Update {
2
    let hit = info.get_hit_node();
2
    let Some(mut data) = refany.downcast_mut::<GalleryCellClickData>() else {
        return Update::DoNothing;
    };
2
    let idx = data.cell_idx;
2
    let auto_select = data.auto_select;
2
    let in_panel = data.in_panel;
2
    let selected_style = data.selected_style.clone();
2
    let base_style = data.base_style.clone();
2
    let user = data.on_select.clone();
2
    drop(data);
    // Default behavior: move the highlight to the clicked cell immediately,
    // so the gallery feels live even if the app does not re-render. The hit
    // node may be the cell's preview or label, so resolve the cell by class.
2
    let cell = ancestor_with_class(&info, hit, GALLERY_CELL_CLASS).unwrap_or(hit);
2
    if auto_select {
        if let Some(strip) = info.get_parent(cell) {
            let mut sibling = info.get_first_child(strip);
            while let Some(cell_node) = sibling {
                let style = if cell_node == cell { &selected_style } else { &base_style };
                for prop in style.as_ref() {
                    if prop.apply_if.as_ref().is_empty() {
                        info.set_css_property(cell_node, prop.property.clone());
                    }
                }
                sibling = info.get_next_sibling(cell_node);
            }
        }
        // Picking from the expansion panel closes it.
        if in_panel {
            if let Some(panel) = info.get_parent(cell) {
                info.set_css_property(panel, P::const_display(LayoutDisplay::None));
            }
        }
2
    }
2
    match user.into_option() {
1
        Some(RibbonGalleryOnSelect { refany, callback }) => {
1
            (callback.cb)(refany, info, idx)
        }
1
        None => Update::DoNothing,
    }
2
}
impl From<Ribbon> for Dom {
1
    fn from(r: Ribbon) -> Self { r.dom() }
}
#[cfg(test)]
mod tests {
    use std::{
        collections::BTreeMap,
        sync::{Arc, Mutex},
    };
    use azul_core::{
        dom::{DomId, DomNodeId, NodeId, NodeType},
        geom::OptionLogicalPosition,
        gl::OptionGlContextPtr,
        hit_test::ScrollPosition,
        refany::OptionRefAny,
        resources::RendererResources,
        styled_dom::NodeHierarchyItemId,
        window::{MonitorVec, RawWindowHandle},
    };
    use azul_css::{props::property::CssProperty, system::SystemStyle};
    use rust_fontconfig::FcFontCache;
    use super::*;
    #[cfg(feature = "icu")]
    use crate::icu::IcuLocalizerHandle;
    use crate::{
        callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
        window::LayoutWindow,
        window_state::FullWindowState,
    };
    // ------------------------------------------------------------------
    // Helpers
    // ------------------------------------------------------------------
227
    fn has_class(node: &Dom, name: &str) -> bool {
227
        node.root
227
            .get_ids_and_classes()
227
            .as_ref()
227
            .iter()
229
            .any(|c| matches!(c, Class(s) if s.as_str() == name))
227
    }
    /// Text of a label node, looking through the `<p>` block wrapper the
    /// label convention mandates (`p > text`).
16
    fn text_of(node: &Dom) -> Option<&str> {
16
        match node.root.get_node_type() {
4
            NodeType::Text(s) => Some(s.as_ref().as_str()),
12
            NodeType::P => match node.children.as_ref() {
12
                [only] => match only.root.get_node_type() {
12
                    NodeType::Text(s) => Some(s.as_ref().as_str()),
                    _ => None,
                },
                _ => None,
            },
            _ => None,
        }
16
    }
    /// USER convention (2026-08-12): widget-emitted text is never a raw
    /// `create_text` child — every label is `<p>` wrapping exactly one text
    /// node. Raw text as a direct flex child takes the anonymous-box path
    /// that made "PAGE LAYOUT" wrap and the group captions de-center live.
    /// Walks the full desktop + mobile chrome (tabs, captions, buttons,
    /// gallery, launcher) and flags every Text node under a non-P parent.
    #[test]
1
    fn every_ribbon_label_is_block_formatted_no_raw_text_children() {
        extern "C" fn noop_launcher_click(
            _data: RefAny,
            _info: CallbackInfo,
        ) -> Update {
            Update::DoNothing
        }
67
        fn walk(node: &Dom, parent_is_p: bool, bad: &mut Vec<String>) {
67
            if let NodeType::Text(t) = node.root.get_node_type() {
13
                if !parent_is_p {
                    bad.push(t.as_ref().as_str().to_string());
13
                }
54
            }
67
            let is_p = matches!(node.root.get_node_type(), NodeType::P);
67
            for c in node.children.as_ref() {
66
                walk(c, is_p, bad);
66
            }
67
        }
1
        let cells = vec![
1
            RibbonGalleryCell::new(
1
                Dom::create_div(), // user preview content — exempt from the convention
1
                "Style 0".into(),
            ),
        ];
1
        let tab = RibbonTab::new("HOME".into())
1
            .with_group(
1
                RibbonGroup::new("Clipboard".into())
1
                    .with_item(RibbonItem::LargeButton(RibbonButton::new(
1
                        "content_paste".into(),
1
                        "Paste".into(),
1
                    )))
1
                    .with_launcher(
1
                        RefAny::new(0usize),
1
                        noop_launcher_click as crate::widgets::button::ButtonOnClickCallbackType,
                    ),
            )
1
            .with_group(
1
                RibbonGroup::new("Styles".into())
1
                    .with_item(RibbonItem::Gallery(RibbonGallery::new(cells.into()))),
            );
1
        let dom = Ribbon::new(RibbonTabVec::from_vec(vec![
1
            tab,
1
            RibbonTab::new("PAGE LAYOUT".into()),
        ]))
1
        .with_app_button(RibbonAppButton::new("FILE".into()))
1
        .dom();
1
        let mut bad = Vec::new();
1
        walk(&dom, false, &mut bad);
1
        assert!(
1
            bad.is_empty(),
            "raw text nodes outside a <p> wrapper: {bad:?}"
        );
1
    }
    /// The text of a box's single label child (tabs / app button).
5
    fn label_text(node: &Dom) -> Option<&str> {
5
        node.children.as_ref().first().and_then(text_of)
5
    }
8
    fn icon_name_of(node: &Dom) -> Option<&str> {
8
        match node.root.get_node_type() {
8
            NodeType::Icon(s) => Some(s.as_ref().as_str()),
            _ => None,
        }
8
    }
10
    fn inline_props(node: &Dom) -> Vec<CssProperty> {
10
        node.root
10
            .style
10
            .iter_inline_properties()
185
            .map(|(p, _)| p.clone())
10
            .collect()
10
    }
11
    fn style_props(style: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
165
        style.as_ref().iter().map(|c| c.property.clone()).collect()
11
    }
108
    fn recursive_descendants(node: &Dom) -> usize {
108
        node.children
108
            .as_ref()
108
            .iter()
108
            .map(|c| 1 + recursive_descendants(c))
108
            .sum()
108
    }
    /// `(tab bar, content)` of a rendered ribbon DOM.
    ///
    /// The root also carries the mobile chrome (tab button, tab overlay,
    /// group list), which the viewport conditions hide on desktop - so the
    /// parts are located by CLASS, not by index.
26
    fn parts(dom: &Dom) -> (&Dom, &Dom) {
52
        let by_class = |name: &str| {
52
            dom.children
52
                .as_ref()
52
                .iter()
156
                .find(|c| has_class(c, name))
52
                .unwrap_or_else(|| panic!("a ribbon DOM has a {name} child"))
52
        };
26
        (
26
            by_class("__azul-native-ribbon-tabbar"),
26
            by_class("__azul-native-ribbon-content"),
26
        )
26
    }
    /// `(items row, footer)` of the `n`-th rendered group.
17
    fn group_parts(content: &Dom, n: usize) -> (&Dom, &Dom) {
17
        let group = &content.children.as_ref()[n];
17
        let ch = group.children.as_ref();
17
        assert_eq!(ch.len(), 2, "a group is exactly [items, footer]");
17
        (&ch[0], &ch[1])
17
    }
19
    fn tabs(n: usize) -> RibbonTabVec {
19
        let mut v = Vec::with_capacity(n);
53
        for i in 0..n {
53
            v.push(RibbonTab::new(AzString::from(format!("t{i}"))));
53
        }
19
        RibbonTabVec::from_vec(v)
19
    }
9
    fn small_btn(icon: &str, label: &str) -> RibbonButton {
9
        RibbonButton::new(AzString::from(icon), AzString::from(label))
9
    }
    struct IndexLog {
        seen: Vec<usize>,
    }
4
    extern "C" fn record_index(mut data: RefAny, _: CallbackInfo, index: usize) -> Update {
4
        if let Some(mut log) = data.downcast_mut::<IndexLog>() {
4
            log.seen.push(index);
4
        }
4
        Update::RefreshDom
4
    }
2
    fn log_indices(data: &mut RefAny) -> Vec<usize> {
2
        data.downcast_ref::<IndexLog>()
2
            .expect("payload must still be an IndexLog")
2
            .seen
2
            .clone()
2
    }
    /// Invokes `cb` (a ribbon trampoline) with a minimal `CallbackInfo`. The
    /// trampolines never read the DOM, so the `LayoutWindow` holds no layout
    /// results - if they ever start touching them, these tests notice.
6
    fn run_trampoline(
6
        cb: extern "C" fn(RefAny, CallbackInfo) -> Update,
6
        data: RefAny,
6
    ) -> (Update, Vec<CallbackChange>) {
6
        let layout_window =
6
            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
6
        let renderer_resources = RendererResources::default();
6
        let previous_window_state: Option<FullWindowState> = None;
6
        let current_window_state = FullWindowState::default();
6
        let gl_context = OptionGlContextPtr::None;
6
        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
6
            BTreeMap::new();
6
        let window_handle = RawWindowHandle::Unsupported;
6
        let system_callbacks = ExternalSystemCallbacks::rust_internal();
6
        let ref_data = CallbackInfoRefData {
6
            layout_window: &layout_window,
6
            renderer_resources: &renderer_resources,
6
            previous_window_state: &previous_window_state,
6
            current_window_state: &current_window_state,
6
            gl_context: &gl_context,
6
            current_scroll_manager: &scroll_states,
6
            current_window_handle: &window_handle,
6
            system_callbacks: &system_callbacks,
6
            system_style: Arc::new(SystemStyle::default()),
6
            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
6
            #[cfg(feature = "icu")]
6
            icu_localizer: IcuLocalizerHandle::default(),
6
            ctx: OptionRefAny::None,
6
        };
6
        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
6
        let info = CallbackInfo::new(
6
            &ref_data,
6
            &changes,
6
            DomNodeId {
6
                dom: DomId::ROOT_ID,
6
                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0))),
6
            },
6
            OptionLogicalPosition::None,
6
            OptionLogicalPosition::None,
        );
6
        let update = cb(data, info);
6
        let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
6
        (update, recorded)
6
    }
    // ------------------------------------------------------------------
    // Constructors and invariants
    // ------------------------------------------------------------------
    #[test]
1
    fn ribbon_new_defaults_to_office_2013_with_tab_zero_active() {
5
        for count in [0usize, 1, 2, 9] {
4
            let r = Ribbon::new(tabs(count));
4
            assert_eq!(r.tabs.len(), count);
4
            assert_eq!(r.active_tab, 0);
4
            assert!(r.on_tab_click.is_none());
4
            assert!(r.app_button.is_none());
4
            assert_eq!(r.style, RibbonStyle::office_2013());
        }
1
    }
    #[test]
1
    fn ribbon_style_default_is_office_2013() {
1
        assert_eq!(RibbonStyle::default(), RibbonStyle::office_2013());
1
    }
    #[test]
1
    fn set_active_tab_clamps_to_the_last_valid_index() {
1
        let mut r = Ribbon::new(RibbonTabVec::from_vec(Vec::new()));
4
        for index in [0usize, 1, usize::MAX / 2, usize::MAX] {
4
            r.set_active_tab(index);
4
            assert_eq!(r.active_tab, 0, "empty ribbon must clamp {index} to 0");
        }
1
        let mut r = Ribbon::new(tabs(4));
5
        for index in 0..4 {
4
            r.set_active_tab(index);
4
            assert_eq!(r.active_tab, index);
        }
4
        for index in [4usize, 5, usize::MAX - 1, usize::MAX] {
4
            r.set_active_tab(index);
4
            assert_eq!(r.active_tab, 3, "{index} must clamp to the last tab");
        }
1
    }
    #[test]
1
    fn group_and_tab_builders_append_in_order() {
1
        let group = RibbonGroup::new(AzString::from("Font"))
1
            .with_item(RibbonItem::SmallButton(small_btn("format_bold", "")))
1
            .with_item(RibbonItem::Separator);
1
        assert_eq!(group.items.len(), 2);
1
        assert!(group.launcher.is_none());
1
        let tab = RibbonTab::new(AzString::from("HOME"))
1
            .with_group(group.clone())
1
            .with_group(RibbonGroup::new(AzString::from("Editing")));
1
        assert_eq!(tab.groups.len(), 2);
1
        assert_eq!(tab.groups.as_ref()[0].label.as_str(), "Font");
1
        assert_eq!(tab.groups.as_ref()[1].label.as_str(), "Editing");
1
    }
    // ------------------------------------------------------------------
    // Tab bar
    // ------------------------------------------------------------------
    #[test]
1
    fn dom_of_an_empty_ribbon_has_only_the_filler_in_the_bar() {
1
        let dom = Ribbon::new(RibbonTabVec::from_vec(Vec::new())).dom();
1
        assert!(has_class(&dom, "__azul-native-ribbon"));
1
        let (bar, content) = parts(&dom);
1
        assert!(has_class(bar, "__azul-native-ribbon-tabbar"));
1
        assert_eq!(bar.children.as_ref().len(), 1, "empty ribbon bar = filler only");
1
        assert!(has_class(&bar.children.as_ref()[0], "__azul-native-ribbon-tab-filler"));
1
        assert!(content.children.as_ref().is_empty());
1
    }
    #[test]
1
    fn dom_desktop_emits_only_the_desktop_chrome() {
1
        let r = Ribbon::new(tabs(3));
1
        let dom = r.dom_desktop();
1
        let ch = dom.children.as_ref();
1
        assert_eq!(ch.len(), 2, "desktop chrome = [tab bar, content]");
1
        assert!(has_class(&ch[0], "__azul-native-ribbon-tabbar"));
1
        assert!(has_class(&ch[1], "__azul-native-ribbon-content"));
1
    }
    #[test]
1
    fn dom_mobile_emits_only_the_touch_chrome_with_one_visible_group() {
        use azul_css::props::property::CssPropertyType;
1
        let mut tab = RibbonTab::new(AzString::from_const_str("HOME"));
3
        for label in ["Clipboard", "Font", "Paragraph"] {
3
            tab = tab.with_group(RibbonGroup::new(AzString::from(label)));
3
        }
1
        let r = Ribbon::new(RibbonTabVec::from_vec(vec![tab]));
1
        let dom = r.dom_mobile();
1
        let ch = dom.children.as_ref();
1
        assert_eq!(ch.len(), 3, "mobile chrome = [tab button, overlay, band]");
1
        assert!(has_class(&ch[0], "__azul-native-ribbon-mobile-tab"));
1
        assert!(has_class(&ch[1], "__azul-native-ribbon-mobile-tab-overlay"));
1
        assert!(has_class(&ch[2], "__azul-native-ribbon-mobile-band"));
1
        let band = ch[2].children.as_ref();
1
        assert_eq!(band.len(), 2, "band = [group list, content] side by side");
1
        assert!(has_class(&band[0], "__azul-native-ribbon-mobile-group-list"));
1
        assert!(has_class(&band[1], "__azul-native-ribbon-content"));
        // Exactly the FIRST group is visible; the others carry an appended
        // unconditional display:none (the group list swaps them at runtime).
3
        let last_uncond_display = |d: &Dom| {
3
            d.root
3
                .style
3
                .iter_inline_properties()
30
                .filter(|(p, conds)| {
30
                    conds.as_ref().is_empty() && p.get_type() == CssPropertyType::Display
30
                })
3
                .last()
3
                .map(|(p, _)| p.clone())
3
        };
1
        let groups = ch[2].children.as_ref()[1].children.as_ref();
1
        assert_eq!(groups.len(), 3);
1
        assert_ne!(
1
            last_uncond_display(&groups[0]),
1
            Some(P::const_display(LayoutDisplay::None)),
            "first group stays visible"
        );
2
        for g in &groups[1..] {
2
            assert_eq!(
2
                last_uncond_display(g),
2
                Some(P::const_display(LayoutDisplay::None)),
                "non-initial groups start hidden in the mobile chrome"
            );
        }
        // Every group-list entry carries the swap callback.
3
        for item in ch[2].children.as_ref()[0].children.as_ref() {
3
            assert_eq!(item.root.callbacks.as_ref().len(), 1, "group-list entry has the swap callback");
        }
1
    }
    #[test]
1
    fn dom_renders_app_button_tabs_and_filler_in_order() {
1
        let r = Ribbon::new(tabs(3))
1
            .with_app_button(RibbonAppButton::new(AzString::from("FILE")))
1
            .with_active_tab(1);
1
        let dom = r.dom();
1
        let (bar, _) = parts(&dom);
1
        let ch = bar.children.as_ref();
1
        assert_eq!(ch.len(), 5, "[app, t0, t1, t2, filler]");
1
        assert!(has_class(&ch[0], "__azul-native-ribbon-appbutton"));
        // The app button and the tabs are BOXES holding a label text child
        // (a raw text node is an inline box, whose border paints around the
        // text run instead of the padded tab).
1
        assert_eq!(label_text(&ch[0]), Some("FILE"));
4
        for i in 0..3 {
3
            assert_eq!(label_text(&ch[1 + i]), Some(format!("t{i}").as_str()));
3
            assert!(has_class(&ch[1 + i], "__azul-native-ribbon-tab"));
3
            assert_eq!(
3
                has_class(&ch[1 + i], "__azul-native-ribbon-tab-active"),
3
                i == 1,
                "only tab 1 is active"
            );
        }
1
        assert!(has_class(&ch[4], "__azul-native-ribbon-tab-filler"));
        // the active tab carries the active style, the others the plain style
1
        let s = RibbonStyle::office_2013();
1
        assert_eq!(inline_props(&ch[1]), style_props(&s.tab_style));
1
        assert_eq!(inline_props(&ch[2]), style_props(&s.tab_active_style));
1
    }
    #[test]
1
    fn dom_with_an_out_of_range_active_tab_highlights_nothing_and_renders_no_groups() {
1
        let mut r = Ribbon::new(tabs(3));
1
        r.active_tab = usize::MAX; // public field bypasses the clamp
1
        let dom = r.dom();
1
        let (bar, content) = parts(&dom);
3
        for tab in &bar.children.as_ref()[..3] {
3
            assert!(!has_class(tab, "__azul-native-ribbon-tab-active"));
        }
1
        assert!(content.children.as_ref().is_empty());
1
    }
    #[test]
1
    fn dom_without_a_callback_attaches_no_user_tab_handler() {
1
        let dom = Ribbon::new(tabs(4)).dom();
1
        let (bar, _) = parts(&dom);
5
        for tab in bar.children.as_ref() {
5
            assert!(
5
                !tab.root
5
                    .get_callbacks()
5
                    .as_ref()
5
                    .iter()
12
                    .any(|c| c.event == EventFilter::Hover(HoverEventFilter::MouseUp)),
                "no user callback -> no MouseUp handler (chrome handlers may still be present)"
            );
        }
1
    }
    #[test]
1
    fn dom_gives_every_tab_a_mouseup_callback_with_its_own_index() {
1
        let dom = Ribbon::new(tabs(5))
1
            .with_on_tab_click(RefAny::new(IndexLog { seen: Vec::new() }), record_index as RibbonOnTabClickCallbackType)
1
            .dom();
1
        let (bar, _) = parts(&dom);
5
        for (i, tab) in bar.children.as_ref()[..5].iter().enumerate() {
5
            let cbs = tab.root.get_callbacks();
            // Default behavior also attaches the collapse/peek chrome
            // handlers; the USER callback is the MouseUp one.
5
            let click = cbs
5
                .as_ref()
5
                .iter()
5
                .find(|c| c.event == EventFilter::Hover(HoverEventFilter::MouseUp))
5
                .expect("every tab has a MouseUp user handler");
5
            let mut payload = click.refany.clone();
5
            let data = payload
5
                .downcast_ref::<TabClickData>()
5
                .expect("tab payload is a TabClickData");
5
            assert_eq!(data.tab_idx, i);
        }
        // the filler has no callback
1
        assert!(bar.children.as_ref()[5].root.get_callbacks().as_ref().is_empty());
1
    }
    #[test]
1
    fn app_button_callback_is_attached_directly() {
        extern "C" fn noop(_: RefAny, _: CallbackInfo) -> Update {
            Update::DoNothing
        }
1
        let ab = RibbonAppButton::new(AzString::from("FILE"))
1
            .with_on_click(RefAny::new(0u8), noop as super::super::button::ButtonOnClickCallbackType);
1
        let dom = Ribbon::new(tabs(1)).with_app_button(ab).dom();
1
        let (bar, _) = parts(&dom);
1
        let cbs = bar.children.as_ref()[0].root.get_callbacks();
1
        assert_eq!(cbs.as_ref().len(), 1);
1
        assert_eq!(cbs.as_ref()[0].callback.cb, noop as usize);
1
    }
    // ------------------------------------------------------------------
    // Trampolines
    // ------------------------------------------------------------------
    #[test]
1
    fn tab_click_forwards_the_index_and_propagates_the_update() {
1
        let mut log = RefAny::new(IndexLog { seen: Vec::new() });
4
        for idx in [0usize, 7, usize::MAX] {
3
            let data = RefAny::new(TabClickData {
3
                tab_idx: idx,
3
                on_tab_click: Some(RibbonOnTabClick {
3
                    callback: (record_index as RibbonOnTabClickCallbackType).into(),
3
                    refany: log.clone(),
3
                })
3
                .into(),
3
            });
3
            let (update, changes) = run_trampoline(on_ribbon_tab_click, data);
3
            assert_eq!(update, Update::RefreshDom);
3
            assert!(changes.is_empty());
        }
1
        assert_eq!(log_indices(&mut log), vec![0, 7, usize::MAX]);
1
    }
    #[test]
1
    fn gallery_click_forwards_the_index_and_propagates_the_update() {
1
        let mut log = RefAny::new(IndexLog { seen: Vec::new() });
1
        let data = RefAny::new(GalleryCellClickData {
1
            cell_idx: 3,
1
            on_select: Some(RibbonGalleryOnSelect {
1
                callback: (record_index as RibbonGalleryOnSelectCallbackType).into(),
1
                refany: log.clone(),
1
            })
1
            .into(),
1
            // The auto-select branch needs live layout results; this test
1
            // drives the forwarding path only.
1
            auto_select: false,
1
            in_panel: false,
1
            selected_style: CssPropertyWithConditionsVec::from_const_slice(&[]),
1
            base_style: CssPropertyWithConditionsVec::from_const_slice(&[]),
1
        });
1
        let (update, changes) = run_trampoline(on_ribbon_gallery_cell_click, data);
1
        assert_eq!(update, Update::RefreshDom);
1
        assert!(changes.is_empty());
1
        assert_eq!(log_indices(&mut log), vec![3]);
1
    }
    #[test]
1
    fn trampolines_with_foreign_or_empty_payloads_are_noops() {
1
        let (update, changes) = run_trampoline(on_ribbon_tab_click, RefAny::new(0xdead_u64));
1
        assert_eq!(update, Update::DoNothing);
1
        assert!(changes.is_empty());
1
        let data = RefAny::new(GalleryCellClickData {
1
            cell_idx: 0,
1
            on_select: None.into(),
1
            auto_select: false,
1
            in_panel: false,
1
            selected_style: CssPropertyWithConditionsVec::from_const_slice(&[]),
1
            base_style: CssPropertyWithConditionsVec::from_const_slice(&[]),
1
        });
1
        let (update, _) = run_trampoline(on_ribbon_gallery_cell_click, data);
1
        assert_eq!(update, Update::DoNothing);
1
    }
    // ------------------------------------------------------------------
    // Groups
    // ------------------------------------------------------------------
    #[test]
1
    fn group_renders_items_over_a_footer_with_the_caption() {
1
        let tab = RibbonTab::new(AzString::from("HOME")).with_group(
1
            RibbonGroup::new(AzString::from("Clipboard"))
1
                .with_item(RibbonItem::SmallButton(small_btn("content_cut", "Cut"))),
        );
1
        let dom = Ribbon::new(RibbonTabVec::from_vec(vec![tab])).dom();
1
        let (_, content) = parts(&dom);
1
        assert_eq!(content.children.as_ref().len(), 1);
1
        assert!(has_class(&content.children.as_ref()[0], "__azul-native-ribbon-group"));
1
        let (items, footer) = group_parts(content, 0);
1
        assert!(has_class(items, "__azul-native-ribbon-group-items"));
1
        assert_eq!(items.children.as_ref().len(), 1);
1
        assert!(has_class(footer, "__azul-native-ribbon-group-footer"));
        // no launcher: the footer is exactly [caption]
1
        assert_eq!(footer.children.as_ref().len(), 1);
1
        assert_eq!(text_of(&footer.children.as_ref()[0]), Some("Clipboard"));
1
    }
    #[test]
1
    fn group_with_launcher_renders_spacer_caption_launcher() {
        extern "C" fn noop(_: RefAny, _: CallbackInfo) -> Update {
            Update::DoNothing
        }
1
        let group = RibbonGroup::new(AzString::from("Font"))
1
            .with_launcher(RefAny::new(0u8), noop as super::super::button::ButtonOnClickCallbackType);
1
        let tab = RibbonTab::new(AzString::from("HOME")).with_group(group);
1
        let dom = Ribbon::new(RibbonTabVec::from_vec(vec![tab])).dom();
1
        let (_, content) = parts(&dom);
1
        let (_, footer) = group_parts(content, 0);
1
        let ch = footer.children.as_ref();
1
        assert_eq!(ch.len(), 3, "[spacer, caption, launcher]");
1
        assert!(has_class(&ch[0], "__azul-native-ribbon-footer-spacer"));
1
        assert_eq!(text_of(&ch[1]), Some("Font"));
        // the launcher is a real Button widget with the south_east icon
1
        assert!(matches!(ch[2].root.get_node_type(), NodeType::Button));
1
        assert_eq!(icon_name_of(&ch[2].children.as_ref()[0]), Some("south_east"));
1
        assert_eq!(ch[2].root.get_callbacks().as_ref().len(), 1);
1
    }
    // ------------------------------------------------------------------
    // Items
    // ------------------------------------------------------------------
    /// Renders one item into a throwaway single-group ribbon and returns the
    /// rendered item node.
12
    fn render_item(item: RibbonItem) -> Dom {
12
        let tab = RibbonTab::new(AzString::from("t"))
12
            .with_group(RibbonGroup::new(AzString::from("g")).with_item(item));
12
        let dom = Ribbon::new(RibbonTabVec::from_vec(vec![tab])).dom();
12
        let (_, content) = parts(&dom);
12
        let (items, _) = group_parts(content, 0);
12
        assert_eq!(items.children.as_ref().len(), 1);
12
        items.children.as_ref()[0].clone()
12
    }
    #[test]
1
    fn large_button_expands_to_a_button_widget_with_icon_label_and_arrow() {
1
        let rb = RibbonButton::new(AzString::from("content_paste"), AzString::from("Paste"))
1
            .with_arrow(RibbonArrow::Split);
1
        let node = render_item(RibbonItem::LargeButton(rb));
1
        assert!(matches!(node.root.get_node_type(), NodeType::Button));
1
        assert!(has_class(&node, "__azul-native-button"), "reuses the Button widget");
1
        let ch = node.children.as_ref();
1
        assert_eq!(ch.len(), 3, "[icon, label, arrow]");
1
        assert_eq!(icon_name_of(&ch[0]), Some("content_paste"));
1
        assert_eq!(text_of(&ch[1]), Some("Paste"));
1
        assert_eq!(icon_name_of(&ch[2]), Some("arrow_drop_down"));
1
        let s = RibbonStyle::office_2013();
1
        assert_eq!(inline_props(&node), style_props(&s.large_button_style));
1
        assert_eq!(inline_props(&ch[0]), style_props(&s.large_icon_style));
1
        assert_eq!(inline_props(&ch[1]), style_props(&s.large_label_style));
1
        assert_eq!(inline_props(&ch[2]), style_props(&s.arrow_icon_style));
1
    }
    #[test]
1
    fn icon_only_small_button_skips_the_empty_label() {
1
        let node = render_item(RibbonItem::SmallButton(small_btn("format_bold", "")));
1
        let ch = node.children.as_ref();
1
        assert_eq!(ch.len(), 1, "icon only — no empty text node");
1
        assert_eq!(icon_name_of(&ch[0]), Some("format_bold"));
1
    }
    #[test]
1
    fn toggled_button_appends_the_checked_style_last() {
1
        let rb = small_btn("format_align_left", "").with_toggled(true);
1
        let node = render_item(RibbonItem::SmallButton(rb));
1
        let s = RibbonStyle::office_2013();
1
        let mut expected = style_props(&s.small_button_style);
1
        expected.extend(style_props(&s.checked_style));
1
        assert_eq!(
1
            inline_props(&node),
            expected,
            "checked props must come last so they win (inline CSS is last-wins)"
        );
1
    }
    #[test]
1
    fn columns_and_rows_nest_items_recursively() {
1
        let column = RibbonColumn::new()
1
            .with_item(RibbonItem::SmallButton(small_btn("content_cut", "Cut")))
1
            .with_item(RibbonItem::Row(
1
                RibbonRow::new()
1
                    .with_item(RibbonItem::SmallButton(small_btn("format_bold", "")))
1
                    .with_item(RibbonItem::Separator),
1
            ));
1
        let node = render_item(RibbonItem::Column(column));
1
        assert!(has_class(&node, "__azul-native-ribbon-column"));
1
        let ch = node.children.as_ref();
1
        assert_eq!(ch.len(), 2);
1
        assert!(matches!(ch[0].root.get_node_type(), NodeType::Button));
1
        assert!(has_class(&ch[1], "__azul-native-ribbon-row"));
1
        let row_ch = ch[1].children.as_ref();
1
        assert_eq!(row_ch.len(), 2);
1
        assert!(has_class(&row_ch[1], "__azul-native-ribbon-separator"));
1
    }
    #[test]
1
    fn embedded_widgets_render_with_their_own_classes() {
        use azul_css::StringVec;
1
        let combo = ComboBox::new(StringVec::from_vec(vec![AzString::from("Calibri")]));
1
        let node = render_item(RibbonItem::Combo(combo));
1
        assert!(has_class(&node, "__azul-native-combobox"));
1
        let drop = DropDown::new(StringVec::from_vec(vec![AzString::from("11")]));
1
        let node = render_item(RibbonItem::Drop(drop));
1
        assert!(has_class(&node, "__azul-native-dropdown"));
1
        let check = CheckBox::create(true);
1
        let node = render_item(RibbonItem::Check(check));
1
        assert!(has_class(&node, "__azul-native-checkbox-container"));
1
    }
    #[test]
1
    fn custom_items_pass_through_verbatim() {
1
        let custom = Dom::create_text_do_not_use_without_block_level_wrapper("¶");
1
        let node = render_item(RibbonItem::Custom(custom.clone()));
1
        assert_eq!(node, custom);
1
    }
    // ------------------------------------------------------------------
    // Gallery
    // ------------------------------------------------------------------
7
    fn gallery(cells: usize) -> RibbonGallery {
7
        let v: Vec<RibbonGalleryCell> = (0..cells)
21
            .map(|i| {
21
                RibbonGalleryCell::new(
21
                    Dom::create_text_do_not_use_without_block_level_wrapper(format!("AaBbCc{i}")),
21
                    AzString::from(format!("Style {i}")),
                )
21
            })
7
            .collect();
7
        RibbonGallery::new(RibbonGalleryCellVec::from_vec(v))
7
    }
    #[test]
1
    fn gallery_renders_strip_cells_and_three_spinner_buttons() {
1
        let wrapper = render_item(RibbonItem::Gallery(gallery(4).with_selected(2)));
1
        assert!(has_class(&wrapper, "__azul-native-ribbon-gallery-wrapper"));
1
        let node = &wrapper.children.as_ref()[0];
1
        assert!(has_class(node, "__azul-native-ribbon-gallery"));
1
        let ch = node.children.as_ref();
1
        assert_eq!(ch.len(), 2, "[strip, spinner]");
1
        let (strip, spinner) = (&ch[0], &ch[1]);
1
        assert!(has_class(strip, "__azul-native-ribbon-gallery-strip"));
1
        let cells = strip.children.as_ref();
1
        assert_eq!(cells.len(), 4);
4
        for (i, cell) in cells.iter().enumerate() {
4
            assert!(has_class(cell, "__azul-native-ribbon-gallery-cell"));
4
            assert_eq!(
4
                has_class(cell, "__azul-native-ribbon-gallery-cell-selected"),
4
                i == 2,
                "only cell 2 is selected"
            );
            // [preview, label]
4
            let cc = cell.children.as_ref();
4
            assert_eq!(cc.len(), 2);
4
            assert_eq!(text_of(&cc[0]), Some(format!("AaBbCc{i}").as_str()));
4
            assert_eq!(text_of(&cc[1]), Some(format!("Style {i}").as_str()));
        }
        // selected cell style = base + selected extras appended
1
        let s = RibbonStyle::office_2013();
1
        let mut expected = style_props(&s.gallery_cell_style);
1
        expected.extend(style_props(&s.gallery_cell_selected_style));
1
        assert_eq!(inline_props(&cells[2]), expected);
1
        assert!(has_class(spinner, "__azul-native-ribbon-gallery-spinner"));
1
        let buttons = spinner.children.as_ref();
1
        assert_eq!(buttons.len(), 3);
1
        let expected_icons = ["expand_less", "expand_more", "arrow_drop_down"];
3
        for (b, expected_icon) in buttons.iter().zip(expected_icons) {
3
            assert!(matches!(b.root.get_node_type(), NodeType::Button));
3
            assert_eq!(icon_name_of(&b.children.as_ref()[0]), Some(expected_icon));
        }
1
    }
    #[test]
1
    fn gallery_cells_carry_their_own_index_in_the_click_payload() {
1
        let g = gallery(2).with_on_select(
1
            RefAny::new(IndexLog { seen: Vec::new() }),
1
            record_index as RibbonGalleryOnSelectCallbackType,
        );
1
        let wrapper = render_item(RibbonItem::Gallery(g));
1
        let frame = &wrapper.children.as_ref()[0];
2
        for (i, cell) in frame.children.as_ref()[0].children.as_ref().iter().enumerate() {
2
            let cbs = cell.root.get_callbacks();
2
            assert_eq!(cbs.as_ref().len(), 1);
2
            let mut payload = cbs.as_ref()[0].refany.clone();
2
            let data = payload
2
                .downcast_ref::<GalleryCellClickData>()
2
                .expect("cell payload is a GalleryCellClickData");
2
            assert_eq!(data.cell_idx, i);
        }
1
    }
    // ------------------------------------------------------------------
    // Style injection
    // ------------------------------------------------------------------
    #[test]
1
    fn replacing_a_part_style_restyles_the_expanded_buttons() {
1
        let injected = CssPropertyWithConditionsVec::from_vec(vec![Cond::simple(
1
            P::const_font_size(StyleFontSize::const_px(99)),
        )]);
1
        let tab = RibbonTab::new(AzString::from("t")).with_group(
1
            RibbonGroup::new(AzString::from("g"))
1
                .with_item(RibbonItem::SmallButton(small_btn("format_bold", ""))),
        );
1
        let mut r = Ribbon::new(RibbonTabVec::from_vec(vec![tab]));
1
        r.style.small_button_style = injected.clone();
1
        let dom = r.dom();
1
        let (_, content) = parts(&dom);
1
        let (items, _) = group_parts(content, 0);
1
        assert_eq!(
1
            inline_props(&items.children.as_ref()[0]),
1
            style_props(&injected),
            "the injected style must reach the expanded Button verbatim"
        );
1
    }
    // ------------------------------------------------------------------
    // Whole-tree invariants
    // ------------------------------------------------------------------
    #[test]
1
    fn estimated_child_count_cache_stays_consistent_for_a_full_ribbon() {
1
        let tab = RibbonTab::new(AzString::from("HOME"))
1
            .with_group(
1
                RibbonGroup::new(AzString::from("Clipboard"))
1
                    .with_item(RibbonItem::LargeButton(
1
                        RibbonButton::new(AzString::from("content_paste"), AzString::from("Paste"))
1
                            .with_arrow(RibbonArrow::Split),
1
                    ))
1
                    .with_item(RibbonItem::Column(
1
                        RibbonColumn::new()
1
                            .with_item(RibbonItem::SmallButton(small_btn("content_cut", "Cut")))
1
                            .with_item(RibbonItem::SmallButton(small_btn("content_copy", "Copy"))),
1
                    )),
            )
1
            .with_group(
1
                RibbonGroup::new(AzString::from("Styles"))
1
                    .with_item(RibbonItem::Gallery(gallery(6))),
            );
1
        let dom = Ribbon::new(RibbonTabVec::from_vec(vec![tab]))
1
            .with_app_button(RibbonAppButton::new(AzString::from("FILE")))
1
            .dom();
1
        assert_eq!(
            dom.estimated_total_children,
1
            recursive_descendants(&dom),
            "cached descendant count desynced from the real tree"
        );
1
    }
    #[test]
1
    fn from_ribbon_for_dom_matches_dom() {
        // Only meaningful without callbacks: every `dom()` call mints fresh
        // per-tab RefAny payloads and two RefAnys never compare equal.
        // Inert: the default behaviors mint a fresh chrome `RefAny` per call
        // and two RefAnys never compare equal.
2
        let inert = || Ribbon::new(tabs(3)).with_behavior(RibbonBehavior::inert());
1
        assert_eq!(Dom::from(inert()), inert().dom());
1
    }
    #[test]
1
    fn styled_combo_box_injects_the_ribbon_field_look() {
        use azul_css::StringVec;
1
        let s = RibbonStyle::office_2013();
1
        let combo = s.styled_combo_box(
1
            StringVec::from_vec(vec![AzString::from("Calibri")]),
1
            AzString::from("Calibri (Body)"),
            133,
        );
1
        let default = ComboBox::create();
1
        assert_ne!(combo.wrapper_style, default.wrapper_style, "wrapper restyled");
1
        assert_ne!(combo.field_style, default.field_style, "field restyled");
1
        assert_eq!(combo.combo_state.inner.text.as_str(), "Calibri (Body)");
        // the width is the LAST wrapper property, so it wins over any base width
1
        let last = combo
1
            .wrapper_style
1
            .as_ref()
1
            .last()
1
            .expect("wrapper style is non-empty");
1
        assert!(
1
            matches!(&last.property, CssProperty::Width(_)),
            "styled_combo_box must append the width last, got {:?}",
            last.property
        );
1
    }
    // ------------------------------------------------------------------
    // Theming
    // ------------------------------------------------------------------
    #[test]
1
    fn from_theme_recolors_the_accent_carrying_parts() {
1
        let neon = ColorU { r: 255, g: 0, b: 128, a: 255 };
1
        let mut theme = RibbonTheme::office_2013();
1
        theme.accent = neon;
1
        let s = RibbonStyle::from_theme(theme);
1
        assert_eq!(s.theme, theme, "the style bundle records its palette");
1
        assert_ne!(s, RibbonStyle::office_2013());
        // The app button's fill is the accent color.
1
        let app_bg = s
1
            .app_button_style
1
            .as_ref()
1
            .iter()
11
            .find_map(|c| match &c.property {
1
                CssProperty::BackgroundContent(b) => b.get_property().cloned(),
10
                _ => None,
11
            })
1
            .expect("app button declares a background");
1
        assert_eq!(
1
            app_bg.as_ref(),
1
            &[StyleBackgroundContent::Color(neon)],
            "the app button fill must follow the theme accent"
        );
        // The active tab's text is the accent color.
1
        let active_text = s
1
            .tab_active_style
1
            .as_ref()
1
            .iter()
12
            .find_map(|c| match &c.property {
1
                CssProperty::TextColor(t) => t.get_property().copied(),
11
                _ => None,
12
            })
1
            .expect("active tab declares a text color");
1
        assert_eq!(active_text.inner, neon);
1
    }
    #[test]
1
    fn office_2013_is_exactly_from_theme_of_the_office_2013_palette() {
1
        assert_eq!(
1
            RibbonStyle::office_2013(),
1
            RibbonStyle::from_theme(RibbonTheme::office_2013()),
            "one source of truth: the named preset is just from_theme"
        );
1
    }
    #[test]
1
    fn from_system_with_no_reported_colors_falls_back_to_office_2013() {
        // SystemStyle::default() may pre-fill platform colors; the fallback
        // contract is about a system that reports NO colors at all.
1
        let mut sys = SystemStyle::default();
1
        sys.colors = system::SystemColors::default();
1
        assert_eq!(RibbonTheme::from_system(sys.clone()), RibbonTheme::office_2013());
1
        assert_eq!(RibbonStyle::from_system(sys), RibbonStyle::office_2013());
1
    }
    #[test]
1
    fn from_system_extracts_reported_colors_and_falls_back_for_the_rest() {
1
        let reported = ColorU { r: 9, g: 99, b: 199, a: 255 };
1
        let mut sys = SystemStyle::default();
1
        sys.colors.accent = Some(reported).into();
1
        let t = RibbonTheme::from_system(sys);
1
        assert_eq!(t.accent, reported, "reported accent must be extracted");
1
        assert_eq!(t.hover_border, reported, "hover border follows the accent");
1
        assert_eq!(
            t.text,
1
            RibbonTheme::office_2013().text,
            "unreported colors fall back to the the Office-2013-era look palette"
        );
1
    }
    // ------------------------------------------------------------------
    // Behaviors
    // ------------------------------------------------------------------
    #[test]
1
    fn default_behavior_is_office_2013_and_inert_disables_everything() {
1
        assert_eq!(RibbonBehavior::default(), RibbonBehavior::office_2013());
1
        let w = RibbonBehavior::office_2013();
1
        assert!(w.collapsible && w.peek_on_hover && w.auto_select_gallery && w.expandable_gallery);
1
        assert!(w.mobile_tab_overlay);
1
        let i = RibbonBehavior::inert();
1
        assert!(!i.collapsible && !i.peek_on_hover && !i.auto_select_gallery && !i.expandable_gallery);
1
        assert!(!i.mobile_tab_overlay);
1
        assert_eq!(Ribbon::new(tabs(1)).behavior, RibbonBehavior::office_2013());
1
    }
    #[test]
1
    fn collapsible_tabs_carry_double_click_and_peek_handlers() {
1
        let dom = Ribbon::new(tabs(3)).dom();
1
        let (bar, _) = parts(&dom);
3
        for tab in &bar.children.as_ref()[..3] {
3
            let events: Vec<EventFilter> = tab
3
                .root
3
                .get_callbacks()
3
                .as_ref()
3
                .iter()
3
                .map(|c| c.event)
3
                .collect();
3
            assert!(
3
                events.contains(&EventFilter::Hover(HoverEventFilter::DoubleClick)),
                "a collapsible ribbon must listen for DoubleClick, got {events:?}"
            );
3
            assert!(events.contains(&EventFilter::Hover(HoverEventFilter::MouseEnter)));
3
            assert!(events.contains(&EventFilter::Hover(HoverEventFilter::MouseLeave)));
        }
1
    }
    #[test]
1
    fn inert_behavior_attaches_no_chrome_handlers() {
1
        let dom = Ribbon::new(tabs(2))
1
            .with_behavior(RibbonBehavior::inert())
1
            .dom();
1
        let (bar, _) = parts(&dom);
2
        for tab in &bar.children.as_ref()[..2] {
2
            assert!(
2
                tab.root.get_callbacks().as_ref().is_empty(),
                "an inert ribbon with no user callback must attach nothing"
            );
        }
1
    }
    #[test]
1
    fn peek_can_be_disabled_while_collapse_stays_on() {
1
        let behavior = RibbonBehavior {
1
            peek_on_hover: false,
1
            ..RibbonBehavior::office_2013()
1
        };
1
        let dom = Ribbon::new(tabs(1)).with_behavior(behavior).dom();
1
        let (bar, _) = parts(&dom);
1
        let events: Vec<EventFilter> = bar.children.as_ref()[0]
1
            .root
1
            .get_callbacks()
1
            .as_ref()
1
            .iter()
1
            .map(|c| c.event)
1
            .collect();
1
        assert_eq!(events, vec![EventFilter::Hover(HoverEventFilter::DoubleClick)]);
1
    }
    #[test]
1
    fn expandable_gallery_wraps_the_frame_and_adds_a_hidden_panel() {
1
        let node = render_item(RibbonItem::Gallery(gallery(3)));
1
        assert!(has_class(&node, "__azul-native-ribbon-gallery-wrapper"));
1
        let ch = node.children.as_ref();
1
        assert_eq!(ch.len(), 2, "[frame, panel]");
1
        assert!(has_class(&ch[0], "__azul-native-ribbon-gallery"));
1
        assert!(has_class(&ch[1], "__azul-native-ribbon-gallery-panel"));
        // The panel holds EVERY cell and starts hidden.
1
        assert_eq!(ch[1].children.as_ref().len(), 3);
1
        let display = inline_props(&ch[1]).into_iter().find_map(|p| match p {
1
            CssProperty::Display(d) => d.get_property().copied(),
            _ => None,
1
        });
1
        assert_eq!(display, Some(LayoutDisplay::None), "the panel starts hidden");
        // The third spinner button is the "More" toggle.
1
        let spinner = &ch[0].children.as_ref()[1];
1
        let more = &spinner.children.as_ref()[2];
1
        assert_eq!(more.root.get_callbacks().as_ref().len(), 1);
1
        assert_eq!(
1
            more.root.get_callbacks().as_ref()[0].event,
            EventFilter::Hover(HoverEventFilter::MouseUp)
        );
1
    }
    #[test]
1
    fn non_expandable_gallery_is_the_bare_frame() {
1
        let tab = RibbonTab::new(AzString::from("t")).with_group(
1
            RibbonGroup::new(AzString::from("g")).with_item(RibbonItem::Gallery(gallery(2))),
        );
1
        let behavior = RibbonBehavior {
1
            expandable_gallery: false,
1
            ..RibbonBehavior::office_2013()
1
        };
1
        let dom = Ribbon::new(RibbonTabVec::from_vec(vec![tab]))
1
            .with_behavior(behavior)
1
            .dom();
1
        let (_, content) = parts(&dom);
1
        let (items, _) = group_parts(content, 0);
1
        let node = &items.children.as_ref()[0];
1
        assert!(has_class(node, "__azul-native-ribbon-gallery"));
1
        assert!(!has_class(node, "__azul-native-ribbon-gallery-wrapper"));
1
    }
    #[test]
1
    fn auto_select_attaches_cell_handlers_even_without_a_user_callback() {
        // The classic behavior moves the highlight on click regardless of the app; with
        // auto_select off and no user callback, nothing is attached.
1
        let node = render_item(RibbonItem::Gallery(gallery(2)));
1
        let strip = &node.children.as_ref()[0].children.as_ref()[0];
2
        for cell in strip.children.as_ref() {
2
            assert_eq!(cell.root.get_callbacks().as_ref().len(), 1);
        }
1
        let tab = RibbonTab::new(AzString::from("t")).with_group(
1
            RibbonGroup::new(AzString::from("g")).with_item(RibbonItem::Gallery(gallery(2))),
        );
1
        let dom = Ribbon::new(RibbonTabVec::from_vec(vec![tab]))
1
            .with_behavior(RibbonBehavior::inert())
1
            .dom();
1
        let (_, content) = parts(&dom);
1
        let (items, _) = group_parts(content, 0);
1
        let strip = &items.children.as_ref()[0].children.as_ref()[0];
2
        for cell in strip.children.as_ref() {
2
            assert!(cell.root.get_callbacks().as_ref().is_empty());
        }
1
    }
    // ------------------------------------------------------------------
    // Responsive / mobile
    // ------------------------------------------------------------------
    /// Both chromes are emitted once and the VIEWPORT decides which shows,
    /// so the mobile ribbon keeps the desktop semantics (same tabs, same
    /// groups) without a second widget tree.
    #[test]
1
    fn mobile_chrome_is_emitted_alongside_the_desktop_chrome() {
1
        let dom = Ribbon::new(tabs(3)).with_active_tab(1).dom();
1
        let ch = dom.children.as_ref();
5
        for class in [
            "__azul-native-ribbon-tabbar",
1
            "__azul-native-ribbon-mobile-tab",
1
            "__azul-native-ribbon-mobile-tab-overlay",
1
            "__azul-native-ribbon-mobile-group-list",
1
            "__azul-native-ribbon-content",
        ] {
5
            assert!(
15
                ch.iter().any(|c| has_class(c, class)),
                "the ribbon must emit a {class} child"
            );
        }
        // The mobile button shows the ACTIVE tab's label.
1
        let btn = ch
1
            .iter()
2
            .find(|c| has_class(c, "__azul-native-ribbon-mobile-tab"))
1
            .expect("mobile tab button");
1
        assert_eq!(label_text(btn), Some("t1"));
1
        assert_eq!(
1
            icon_name_of(&btn.children.as_ref()[1]),
            Some("expand_more"),
            "the mobile tab button carries the picker chevron"
        );
        // The overlay lists every tab.
1
        let overlay = ch
1
            .iter()
3
            .find(|c| has_class(c, "__azul-native-ribbon-mobile-tab-overlay"))
1
            .expect("tab overlay");
1
        assert_eq!(overlay.children.as_ref().len(), 3);
1
    }
    /// The breakpoint is expressed as a real viewport condition, and the
    /// conditional value comes LAST so it wins (inline CSS is last-match).
    #[test]
1
    fn the_desktop_tab_strip_is_hidden_under_the_mobile_breakpoint() {
1
        let s = RibbonStyle::office_2013();
1
        let displays: Vec<(&LayoutDisplay, bool)> = s
1
            .tab_bar_style
1
            .as_ref()
1
            .iter()
7
            .filter_map(|c| match &c.property {
2
                CssProperty::Display(d) => {
2
                    d.get_property().map(|d| (d, !c.apply_if.as_ref().is_empty()))
                }
5
                _ => None,
7
            })
1
            .collect();
1
        assert_eq!(
1
            displays.len(),
            2,
            "the tab strip declares an unconditional and a mobile display"
        );
1
        assert_eq!(*displays[0].0, LayoutDisplay::Flex);
1
        assert!(!displays[0].1, "the desktop value is unconditional");
1
        assert_eq!(*displays[1].0, LayoutDisplay::None);
1
        assert!(displays[1].1, "the mobile value is conditional and comes last");
        // ...and the mobile button is the mirror image.
1
        let mobile: Vec<(&LayoutDisplay, bool)> = s
1
            .mobile_tab_button_style
1
            .as_ref()
1
            .iter()
19
            .filter_map(|c| match &c.property {
2
                CssProperty::Display(d) => {
2
                    d.get_property().map(|d| (d, !c.apply_if.as_ref().is_empty()))
                }
17
                _ => None,
19
            })
1
            .collect();
1
        assert_eq!(*mobile[0].0, LayoutDisplay::None);
1
        assert_eq!(*mobile[1].0, LayoutDisplay::Flex);
1
        assert!(mobile[1].1);
1
    }
    /// Handedness moves the mobile group list to the reachable side. It is
    /// independent of text direction, so it is its own system setting.
    #[test]
1
    fn handedness_flips_the_mobile_group_list_divider() {
1
        let right = RibbonStyle::from_theme_handed(RibbonTheme::office_2013(), Handedness::RightHanded);
1
        let left = RibbonStyle::from_theme_handed(RibbonTheme::office_2013(), Handedness::LeftHanded);
1
        assert_ne!(right.mobile_group_list_style, left.mobile_group_list_style);
2
        let has = |s: &CssPropertyWithConditionsVec, want_left: bool| {
22
            s.as_ref().iter().any(|c| {
22
                if want_left {
11
                    matches!(c.property, CssProperty::BorderLeftWidth(_))
                } else {
11
                    matches!(c.property, CssProperty::BorderRightWidth(_))
                }
22
            })
2
        };
1
        assert!(has(&right.mobile_group_list_style, true),
            "a right-handed list sits at the right edge, so its divider is on its LEFT");
1
        assert!(has(&left.mobile_group_list_style, false),
            "a left-handed list sits at the left edge, so its divider is on its RIGHT");
1
    }
    #[test]
1
    fn from_system_picks_up_the_system_handedness() {
1
        let mut sys = SystemStyle::default();
1
        sys.handedness = Handedness::LeftHanded;
1
        let from_sys = RibbonStyle::from_system(sys.clone());
1
        let expected = RibbonStyle::from_theme_handed(
1
            RibbonTheme::from_system(sys),
1
            Handedness::LeftHanded,
        );
1
        assert_eq!(from_sys.mobile_group_list_style, expected.mobile_group_list_style);
1
    }
    #[test]
1
    fn inert_behavior_leaves_the_mobile_tab_button_without_a_toggle() {
1
        let dom = Ribbon::new(tabs(2))
1
            .with_behavior(RibbonBehavior::inert())
1
            .dom();
1
        let btn = dom
1
            .children
1
            .as_ref()
1
            .iter()
2
            .find(|c| has_class(c, "__azul-native-ribbon-mobile-tab"))
1
            .expect("mobile tab button");
1
        assert!(btn.root.get_callbacks().as_ref().is_empty());
1
    }
    #[test]
1
    fn styled_combo_box_follows_the_style_bundles_theme() {
1
        let neon = ColorU { r: 1, g: 2, b: 3, a: 255 };
1
        let mut theme = RibbonTheme::office_2013();
1
        theme.field_border = neon;
1
        let combo = RibbonStyle::from_theme(theme).styled_combo_box(
1
            StringVec::from_vec(vec![]),
1
            AzString::from("x"),
            50,
        );
1
        let border_color = combo
1
            .field_style
1
            .as_ref()
1
            .iter()
22
            .find_map(|c| match &c.property {
1
                CssProperty::BorderTopColor(b) => b.get_property().copied(),
21
                _ => None,
22
            })
1
            .expect("combo field declares a border color");
1
        assert_eq!(border_color.inner, neon, "combo field border follows the theme");
1
    }
}