1
//! Microsoft Office-style backstage view widget (the full-window "FILE"
2
//! screen, the Office-2013-era look look by default).
3
//!
4
//! Models the component hierarchy of the Office backstage:
5
//!
6
//! ```text
7
//! Backstage ─ nav column (dark accent, full height)
8
//!           │    ├─ back button (white ring + left arrow)
9
//!           │    └─ nav items ("Info", "New", "Open", …, "Account", "Options")
10
//!           └─ right side
11
//!                ├─ title strip (optional, app-provided: window title/buttons)
12
//!                └─ content pane (app-provided Dom for the active item)
13
//! ```
14
//!
15
//! The widget owns the CHROME: nav column, back button, item highlight and
16
//! the content host. The per-item pane content ("Open" recent list, "Info"
17
//! properties, …) is application composition, injected through
18
//! [`Backstage::content`] — the backstage does not model document state.
19
//!
20
//! The back button expands to the existing [`super::button::Button`] widget
21
//! with backstage part styles injected (the ribbon's composition rule), and
22
//! its arrow uses `Dom::create_icon("arrow_back")` so glyphs resolve through
23
//! the registered icon provider (Material Icons by default).
24
//!
25
//! All visual parts are exposed on [`BackstageStyle`] (defaults = the Office-2013-era look
26
//! look, [`BackstageStyle::office_2013`]); replace any field to re-theme
27
//! without touching widget code. [`BackstageBehavior`] holds the
28
//! interactions the backstage performs by itself (currently: Escape invokes
29
//! the back callback, like classic office suites).
30

            
31
use azul_core::{
32
    callbacks::{CoreCallback, CoreCallbackData, Update},
33
    dom::{
34
        Dom, DomVec, EventFilter, HoverEventFilter, IdOrClass, IdOrClass::Class, IdOrClassVec,
35
        WindowEventFilter,
36
    },
37
    refany::RefAny,
38
    window::VirtualKeyCode,
39
};
40
#[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
41
use azul_css::{
42
    dynamic_selector::{CssPropertyWithConditions as Cond, CssPropertyWithConditionsVec},
43
    props::{
44
        basic::{color::ColorU, font::{StyleFontFamily, StyleFontFamilyVec}, *},
45
        layout::*,
46
        property::CssProperty as P,
47
        style::*,
48
    },
49
    *,
50
};
51

            
52
use azul_css::{impl_option, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_mut};
53

            
54
use crate::callbacks::CallbackInfo;
55

            
56
use super::button::{Button, ButtonOnClick, OptionButtonOnClick};
57

            
58
// -- Callbacks --
59

            
60
/// Callback signature invoked when a nav item is clicked (receives the item
61
/// index).
62
pub type BackstageOnNavSelectCallbackType =
63
    extern "C" fn(RefAny, CallbackInfo, usize) -> Update;
64
impl_widget_callback!(
65
    BackstageOnNavSelect, OptionBackstageOnNavSelect,
66
    BackstageOnNavSelectCallback, BackstageOnNavSelectCallbackType
67
);
68

            
69
azul_core::impl_managed_callback! {
70
    wrapper:        BackstageOnNavSelectCallback,
71
    info_ty:        CallbackInfo,
72
    return_ty:      Update,
73
    default_ret:    Update::DoNothing,
74
    invoker_static: BACKSTAGE_ON_NAV_SELECT_INVOKER,
75
    invoker_ty:     AzBackstageOnNavSelectCallbackInvoker,
76
    thunk_fn:       az_backstage_on_nav_select_callback_thunk,
77
    setter_fn:      AzApp_setBackstageOnNavSelectCallbackInvoker,
78
    from_handle_fn: AzBackstageOnNavSelectCallback_createFromHostHandle,
79
    extra_args:     [ item_index: usize ],
80
}
81

            
82
// -- Font --
83

            
84
const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
85
const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
86
const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
87
    StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
88

            
89
// -- the Office-2013-era look palette (seeds BackstageTheme::office_2013) --
90

            
91
const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
92
const TRANSPARENT: ColorU = ColorU { r: 0, g: 0, b: 0, a: 0 };
93
/// Office 2013 accent blue (#2B579A): the nav column fill.
94
const W13_BLUE: ColorU = ColorU { r: 43, g: 87, b: 154, a: 255 };
95
/// Hover fill on nav items (#3465AC).
96
const W13_NAV_HOVER: ColorU = ColorU { r: 52, g: 101, b: 172, a: 255 };
97
/// Active nav item fill (#3E6DB5).
98
const W13_NAV_ACTIVE: ColorU = ColorU { r: 62, g: 109, b: 181, a: 255 };
99

            
100
// -- Metrics (the Office-2013-era look, logical px) --
101

            
102
/// Nav column width.
103
const NAV_WIDTH: isize = 126;
104
/// Height of one nav item.
105
const NAV_ITEM_H: isize = 38;
106
/// Nav item text size.
107
const NAV_TEXT_PX: isize = 13;
108
/// Extra gap above a `gap_before` item (office-2013: before "Account").
109
const NAV_GAP_H: isize = 22;
110
/// Back button ring diameter.
111
const BACK_D: isize = 38;
112

            
113
// -- Theme --
114

            
115
/// Color palette from which a full [`BackstageStyle`] is derived via
116
/// [`BackstageStyle::from_theme`]. All fields are plain colors, so themes
117
/// are trivially constructible over FFI. Preset:
118
/// [`BackstageTheme::office_2013`] (the default).
119
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
120
#[repr(C)]
121
pub struct BackstageTheme {
122
    /// Nav column fill (office-2013: accent blue).
123
    pub nav_bg: ColorU,
124
    /// Nav item text and back-arrow color.
125
    pub nav_text: ColorU,
126
    /// Hover fill on nav items.
127
    pub nav_hover_bg: ColorU,
128
    /// Fill of the active nav item.
129
    pub nav_active_bg: ColorU,
130
    /// Content pane fill.
131
    pub content_bg: ColorU,
132
    /// Back button ring color.
133
    pub back_ring: ColorU,
134
}
135

            
136
impl BackstageTheme {
137
    /// The the Office-2013-era look palette: #2B579A nav, white text, lighter-blue
138
    /// highlights, white content.
139
    #[must_use]
140
13
    pub const fn office_2013() -> Self {
141
13
        Self {
142
13
            nav_bg: W13_BLUE,
143
13
            nav_text: WHITE,
144
13
            nav_hover_bg: W13_NAV_HOVER,
145
13
            nav_active_bg: W13_NAV_ACTIVE,
146
13
            content_bg: WHITE,
147
13
            back_ring: WHITE,
148
13
        }
149
13
    }
150
}
151

            
152
impl Default for BackstageTheme {
153
    fn default() -> Self {
154
        Self::office_2013()
155
    }
156
}
157

            
158
// -- Theme -> property-list builders --
159

            
160
117
fn bg_vec(c: ColorU) -> StyleBackgroundContentVec {
161
117
    StyleBackgroundContentVec::from_vec(vec![StyleBackgroundContent::Color(c)])
162
117
}
163

            
164
91
fn cond_bg(c: ColorU) -> Cond {
165
91
    Cond::simple(P::const_background_content(bg_vec(c)))
166
91
}
167

            
168
26
fn cond_bg_hover(c: ColorU) -> Cond {
169
26
    Cond::on_hover(P::const_background_content(bg_vec(c)))
170
26
}
171

            
172
26
const fn cond_text_color(c: ColorU) -> Cond {
173
26
    Cond::simple(P::const_text_color(StyleTextColor { inner: c }))
174
26
}
175

            
176
78
const fn cond_border_box() -> Cond {
177
78
    Cond::simple(P::const_box_sizing(LayoutBoxSizing::BorderBox))
178
78
}
179

            
180
13
fn push_ring_border(v: &mut Vec<Cond>, c: ColorU, width: isize, radius: isize) {
181
13
    v.push(Cond::simple(P::const_border_top_width(LayoutBorderTopWidth::const_px(width))));
182
13
    v.push(Cond::simple(P::const_border_left_width(LayoutBorderLeftWidth::const_px(width))));
183
13
    v.push(Cond::simple(P::const_border_right_width(LayoutBorderRightWidth::const_px(width))));
184
13
    v.push(Cond::simple(P::const_border_bottom_width(LayoutBorderBottomWidth::const_px(width))));
185
13
    v.push(Cond::simple(P::const_border_top_style(StyleBorderTopStyle { inner: BorderStyle::Solid })));
186
13
    v.push(Cond::simple(P::const_border_left_style(StyleBorderLeftStyle { inner: BorderStyle::Solid })));
187
13
    v.push(Cond::simple(P::const_border_right_style(StyleBorderRightStyle { inner: BorderStyle::Solid })));
188
13
    v.push(Cond::simple(P::const_border_bottom_style(StyleBorderBottomStyle { inner: BorderStyle::Solid })));
189
13
    v.push(Cond::simple(P::const_border_top_color(StyleBorderTopColor { inner: c })));
190
13
    v.push(Cond::simple(P::const_border_left_color(StyleBorderLeftColor { inner: c })));
191
13
    v.push(Cond::simple(P::const_border_right_color(StyleBorderRightColor { inner: c })));
192
13
    v.push(Cond::simple(P::const_border_bottom_color(StyleBorderBottomColor { inner: c })));
193
13
    v.push(Cond::simple(P::const_border_top_left_radius(StyleBorderTopLeftRadius::const_px(radius))));
194
13
    v.push(Cond::simple(P::const_border_top_right_radius(StyleBorderTopRightRadius::const_px(radius))));
195
13
    v.push(Cond::simple(P::const_border_bottom_left_radius(StyleBorderBottomLeftRadius::const_px(radius))));
196
13
    v.push(Cond::simple(P::const_border_bottom_right_radius(StyleBorderBottomRightRadius::const_px(radius))));
197
13
}
198

            
199
13
fn theme_root(t: &BackstageTheme) -> CssPropertyWithConditionsVec {
200
13
    CssPropertyWithConditionsVec::from_vec(vec![
201
13
        cond_border_box(),
202
13
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
203
13
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
204
13
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
205
13
        Cond::simple(P::const_font_family(SYSTEM_UI_FAMILY)),
206
13
        Cond::simple(P::const_font_size(StyleFontSize::const_px(NAV_TEXT_PX))),
207
13
        cond_bg(t.content_bg),
208
    ])
209
13
}
210

            
211
13
fn theme_nav(t: &BackstageTheme) -> CssPropertyWithConditionsVec {
212
13
    CssPropertyWithConditionsVec::from_vec(vec![
213
13
        cond_border_box(),
214
13
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
215
13
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
216
13
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
217
13
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
218
13
        Cond::simple(P::const_width(LayoutWidth::const_px(NAV_WIDTH))),
219
13
        cond_bg(t.nav_bg),
220
    ])
221
13
}
222

            
223
/// The circled back arrow. office-2013: a 2px white ring, transparent fill,
224
/// hover fills like a nav item.
225
13
fn theme_back_button(t: &BackstageTheme) -> CssPropertyWithConditionsVec {
226
13
    let mut v = vec![
227
13
        cond_border_box(),
228
13
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
229
13
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
230
13
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
231
13
        Cond::simple(P::const_justify_content(LayoutJustifyContent::Center)),
232
13
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
233
13
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
234
13
        Cond::simple(P::const_width(LayoutWidth::const_px(BACK_D))),
235
13
        Cond::simple(P::const_height(LayoutHeight::const_px(BACK_D))),
236
13
        Cond::simple(P::const_margin_top(LayoutMarginTop::const_px(16))),
237
13
        Cond::simple(P::const_margin_left(LayoutMarginLeft::const_px(20))),
238
13
        Cond::simple(P::const_margin_bottom(LayoutMarginBottom::const_px(18))),
239
13
        Cond::simple(P::const_cursor(StyleCursor::Pointer)),
240
13
        Cond::simple(P::user_select(StyleUserSelect::None)),
241
13
        cond_bg(TRANSPARENT),
242
13
        cond_bg_hover(t.nav_hover_bg),
243
    ];
244
13
    push_ring_border(&mut v, t.back_ring, 2, BACK_D / 2);
245
13
    CssPropertyWithConditionsVec::from_vec(v)
246
13
}
247

            
248
13
fn theme_back_icon(t: &BackstageTheme) -> CssPropertyWithConditionsVec {
249
13
    CssPropertyWithConditionsVec::from_vec(vec![
250
13
        Cond::simple(P::const_font_size(StyleFontSize::const_px(20))),
251
13
        cond_text_color(t.nav_text),
252
    ])
253
13
}
254

            
255
13
fn theme_nav_item(t: &BackstageTheme) -> CssPropertyWithConditionsVec {
256
13
    CssPropertyWithConditionsVec::from_vec(vec![
257
13
        cond_border_box(),
258
13
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
259
13
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
260
13
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
261
13
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
262
13
        Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })),
263
13
        Cond::simple(P::const_height(LayoutHeight::const_px(NAV_ITEM_H))),
264
13
        Cond::simple(P::const_padding_left(LayoutPaddingLeft::const_px(24))),
265
13
        Cond::simple(P::const_font_size(StyleFontSize::const_px(NAV_TEXT_PX))),
266
13
        Cond::simple(P::const_cursor(StyleCursor::Pointer)),
267
13
        Cond::simple(P::user_select(StyleUserSelect::None)),
268
13
        cond_text_color(t.nav_text),
269
13
        cond_bg(TRANSPARENT),
270
13
        cond_bg_hover(t.nav_hover_bg),
271
    ])
272
13
}
273

            
274
/// APPENDED to the active nav item.
275
13
fn theme_nav_item_active(t: &BackstageTheme) -> CssPropertyWithConditionsVec {
276
13
    CssPropertyWithConditionsVec::from_vec(vec![cond_bg(t.nav_active_bg)])
277
13
}
278

            
279
/// APPENDED to a `gap_before` nav item (office-2013: the gap before "Account").
280
13
fn theme_nav_item_gap(_t: &BackstageTheme) -> CssPropertyWithConditionsVec {
281
13
    CssPropertyWithConditionsVec::from_vec(vec![Cond::simple(P::const_margin_top(
282
13
        LayoutMarginTop::const_px(NAV_GAP_H),
283
    ))])
284
13
}
285

            
286
13
fn theme_right(t: &BackstageTheme) -> CssPropertyWithConditionsVec {
287
13
    CssPropertyWithConditionsVec::from_vec(vec![
288
13
        cond_border_box(),
289
13
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
290
13
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
291
13
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
292
13
        cond_bg(t.content_bg),
293
    ])
294
13
}
295

            
296
13
fn theme_content(t: &BackstageTheme) -> CssPropertyWithConditionsVec {
297
13
    CssPropertyWithConditionsVec::from_vec(vec![
298
13
        cond_border_box(),
299
13
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
300
13
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
301
13
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
302
13
        cond_bg(t.content_bg),
303
    ])
304
13
}
305

            
306
// -- Style --
307

            
308
/// All part styles of the backstage. Every part defaults to the the Office-2013-era look
309
/// look; replace any field for finer control (the same override API as
310
/// [`super::ribbon::RibbonStyle`]).
311
#[derive(Debug, Clone, PartialEq, Eq)]
312
#[repr(C)]
313
pub struct BackstageStyle {
314
    /// The palette this style bundle was derived from. Kept for consumers
315
    /// deriving matching custom parts.
316
    pub theme: BackstageTheme,
317
    /// Root container (horizontal: nav column beside the right side).
318
    pub root_style: CssPropertyWithConditionsVec,
319
    /// The nav column.
320
    pub nav_style: CssPropertyWithConditionsVec,
321
    /// Container style injected into the back [`Button`] (the ring).
322
    pub back_button_style: CssPropertyWithConditionsVec,
323
    /// Icon style injected into the back [`Button`] (the arrow).
324
    pub back_icon_style: CssPropertyWithConditionsVec,
325
    /// One nav item.
326
    pub nav_item_style: CssPropertyWithConditionsVec,
327
    /// APPENDED to the active nav item.
328
    pub nav_item_active_style: CssPropertyWithConditionsVec,
329
    /// APPENDED to a `gap_before` nav item.
330
    pub nav_item_gap_style: CssPropertyWithConditionsVec,
331
    /// The right side (title strip over content).
332
    pub right_style: CssPropertyWithConditionsVec,
333
    /// The content host for the active pane.
334
    pub content_style: CssPropertyWithConditionsVec,
335
}
336

            
337
impl BackstageStyle {
338
    /// The the Office-2013-era look look (#2B579A nav, white content) - the default.
339
    #[must_use]
340
13
    pub fn office_2013() -> Self {
341
13
        Self::from_theme(BackstageTheme::office_2013())
342
13
    }
343

            
344
    /// Derives every part style from the given palette.
345
    #[must_use]
346
13
    pub fn from_theme(theme: BackstageTheme) -> Self {
347
13
        let t = &theme;
348
13
        Self {
349
13
            theme,
350
13
            root_style: theme_root(t),
351
13
            nav_style: theme_nav(t),
352
13
            back_button_style: theme_back_button(t),
353
13
            back_icon_style: theme_back_icon(t),
354
13
            nav_item_style: theme_nav_item(t),
355
13
            nav_item_active_style: theme_nav_item_active(t),
356
13
            nav_item_gap_style: theme_nav_item_gap(t),
357
13
            right_style: theme_right(t),
358
13
            content_style: theme_content(t),
359
13
        }
360
13
    }
361
}
362

            
363
impl Default for BackstageStyle {
364
1
    fn default() -> Self {
365
1
        Self::office_2013()
366
1
    }
367
}
368

            
369
// -- Behavior --
370

            
371
/// The interactions the backstage performs BY ITSELF. Each is the classic
372
/// default and each can be turned off.
373
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
374
#[repr(C)]
375
pub struct BackstageBehavior {
376
    /// Pressing Escape invokes the back callback (office-2013: Esc leaves the
377
    /// backstage). Attached as a window-level key handler on the root, so
378
    /// it fires regardless of focus. Requires [`Backstage::on_back`].
379
    pub close_on_escape: bool,
380
}
381

            
382
impl BackstageBehavior {
383
    /// All classic office-suite behaviors enabled - the default.
384
    #[must_use]
385
14
    pub const fn office_2013() -> Self {
386
14
        Self { close_on_escape: true }
387
14
    }
388

            
389
    /// Every self-driven behavior off.
390
    #[must_use]
391
2
    pub const fn inert() -> Self {
392
2
        Self { close_on_escape: false }
393
2
    }
394
}
395

            
396
impl Default for BackstageBehavior {
397
1
    fn default() -> Self {
398
1
        Self::office_2013()
399
1
    }
400
}
401

            
402
// -- Data model --
403

            
404
/// One backstage nav item ("Info", "Open", …).
405
#[derive(Debug, Clone, PartialEq, Eq)]
406
#[repr(C)]
407
pub struct BackstageNavItem {
408
    /// The item label.
409
    pub label: AzString,
410
    /// Renders an extra gap above this item (office-2013: before "Account").
411
    pub gap_before: bool,
412
}
413

            
414
impl BackstageNavItem {
415
    /// Creates a nav item without a gap.
416
    #[must_use]
417
103
    pub const fn new(label: AzString) -> Self {
418
103
        Self { label, gap_before: false }
419
103
    }
420

            
421
    /// Builder method: marks this item as starting a new group.
422
    #[must_use]
423
9
    pub const fn with_gap_before(mut self) -> Self {
424
9
        self.gap_before = true;
425
9
        self
426
9
    }
427
}
428

            
429
impl_option!(
430
    BackstageNavItem,
431
    OptionBackstageNavItem,
432
    copy = false,
433
    [Debug, Clone, PartialEq]
434
);
435
impl_vec!(
436
    BackstageNavItem,
437
    BackstageNavItemVec,
438
    BackstageNavItemVecDestructor,
439
    BackstageNavItemVecDestructorType,
440
    BackstageNavItemVecSlice,
441
    OptionBackstageNavItem
442
);
443
impl_vec_clone!(BackstageNavItem, BackstageNavItemVec, BackstageNavItemVecDestructor);
444
impl_vec_debug!(BackstageNavItem, BackstageNavItemVec);
445
impl_vec_mut!(BackstageNavItem, BackstageNavItemVec);
446

            
447
/// Top-level backstage widget: nav column + app-provided content pane.
448
#[derive(Debug, Clone)]
449
#[repr(C)]
450
pub struct Backstage {
451
    /// Nav items, top to bottom.
452
    pub nav_items: BackstageNavItemVec,
453
    /// Index of the active (highlighted) nav item.
454
    pub active_item: usize,
455
    /// Optional callback fired when a nav item is clicked (receives the
456
    /// item index).
457
    pub on_nav_select: OptionBackstageOnNavSelect,
458
    /// Optional callback fired by the back button (and by Escape, if
459
    /// [`BackstageBehavior::close_on_escape`] is set).
460
    pub on_back: OptionButtonOnClick,
461
    /// Optional strip rendered above the content, right of the nav column
462
    /// (office-2013: the white title bar area with the window buttons).
463
    pub title_strip: azul_core::dom::OptionDom,
464
    /// The active item's pane content (application composition).
465
    pub content: azul_core::dom::OptionDom,
466
    /// Which interactions the backstage handles by itself (defaults to
467
    /// Word).
468
    pub behavior: BackstageBehavior,
469
    /// All part styles (defaults to the the Office-2013-era look look).
470
    pub style: BackstageStyle,
471
}
472

            
473
// -- CSS classes --
474

            
475
static CLS_BACKSTAGE: &[IdOrClass] =
476
    &[Class(AzString::from_const_str("__azul-native-backstage"))];
477
static CLS_NAV: &[IdOrClass] =
478
    &[Class(AzString::from_const_str("__azul-native-backstage-nav"))];
479
static CLS_NAV_ITEM: &[IdOrClass] =
480
    &[Class(AzString::from_const_str("__azul-native-backstage-nav-item"))];
481
static CLS_NAV_ITEM_ACTIVE: &[IdOrClass] = &[
482
    Class(AzString::from_const_str("__azul-native-backstage-nav-item")),
483
    Class(AzString::from_const_str("__azul-native-backstage-nav-item-active")),
484
];
485
static CLS_RIGHT: &[IdOrClass] =
486
    &[Class(AzString::from_const_str("__azul-native-backstage-right"))];
487
static CLS_CONTENT: &[IdOrClass] =
488
    &[Class(AzString::from_const_str("__azul-native-backstage-content"))];
489

            
490
/// The default the Office-2013-era look nav labels, in order.
491
pub const OFFICE_2013_NAV_LABELS: &[&str] = &[
492
    "Info", "New", "Open", "Save", "Save As", "Print", "Share", "Export", "Close",
493
];
494

            
495
// -- Constructors / builders --
496

            
497
impl Backstage {
498
    /// Creates a backstage with the given nav items, item 0 active, no
499
    /// callbacks and no content, in the the Office-2013-era look style.
500
    #[must_use]
501
11
    pub fn new(nav_items: BackstageNavItemVec) -> Self {
502
11
        Self {
503
11
            nav_items,
504
11
            active_item: 0,
505
11
            on_nav_select: None.into(),
506
11
            on_back: None.into(),
507
11
            title_strip: None.into(),
508
11
            content: None.into(),
509
11
            behavior: BackstageBehavior::office_2013(),
510
11
            style: BackstageStyle::office_2013(),
511
11
        }
512
11
    }
513

            
514
    /// The the Office-2013-era look nav: Info / New / Open / Save / Save As / Print /
515
    /// Share / Export / Close, then a gap, then Account / Options.
516
    #[must_use]
517
9
    pub fn office_2013() -> Self {
518
9
        let mut items: Vec<BackstageNavItem> = OFFICE_2013_NAV_LABELS
519
9
            .iter()
520
81
            .map(|l| BackstageNavItem::new(AzString::from(*l)))
521
9
            .collect();
522
9
        items.push(BackstageNavItem::new(AzString::from_const_str("Account")).with_gap_before());
523
9
        items.push(BackstageNavItem::new(AzString::from_const_str("Options")));
524
9
        Self::new(BackstageNavItemVec::from_vec(items))
525
9
    }
526

            
527
    /// Sets the active nav item.
528
1
    pub const fn set_active_item(&mut self, active_item: usize) {
529
1
        self.active_item = active_item;
530
1
    }
531

            
532
    /// Builder method: sets the active nav item and returns `self`.
533
    #[must_use]
534
1
    pub const fn with_active_item(mut self, active_item: usize) -> Self {
535
1
        self.set_active_item(active_item);
536
1
        self
537
1
    }
538

            
539
    /// Sets the pane content for the active item.
540
    pub fn set_content(&mut self, content: Dom) {
541
        self.content = Some(content).into();
542
    }
543

            
544
    /// Builder method: sets the pane content and returns `self`.
545
    #[must_use]
546
    pub fn with_content(mut self, content: Dom) -> Self {
547
        self.set_content(content);
548
        self
549
    }
550

            
551
    /// Sets the title strip rendered above the content.
552
1
    pub fn set_title_strip(&mut self, title_strip: Dom) {
553
1
        self.title_strip = Some(title_strip).into();
554
1
    }
555

            
556
    /// Builder method: sets the title strip and returns `self`.
557
    #[must_use]
558
1
    pub fn with_title_strip(mut self, title_strip: Dom) -> Self {
559
1
        self.set_title_strip(title_strip);
560
1
        self
561
1
    }
562

            
563
    /// Sets the nav-select callback.
564
1
    pub fn set_on_nav_select<C: Into<BackstageOnNavSelectCallback>>(
565
1
        &mut self,
566
1
        data: RefAny,
567
1
        on_nav_select: C,
568
1
    ) {
569
1
        self.on_nav_select = Some(BackstageOnNavSelect {
570
1
            refany: data,
571
1
            callback: on_nav_select.into(),
572
1
        })
573
1
        .into();
574
1
    }
575

            
576
    /// Builder method: sets the nav-select callback and returns `self`.
577
    #[must_use]
578
1
    pub fn with_on_nav_select<C: Into<BackstageOnNavSelectCallback>>(
579
1
        mut self,
580
1
        data: RefAny,
581
1
        on_nav_select: C,
582
1
    ) -> Self {
583
1
        self.set_on_nav_select(data, on_nav_select);
584
1
        self
585
1
    }
586

            
587
    /// Sets the back callback (back button + Escape).
588
2
    pub fn set_on_back<C: Into<super::button::ButtonOnClickCallback>>(
589
2
        &mut self,
590
2
        data: RefAny,
591
2
        on_back: C,
592
2
    ) {
593
2
        self.on_back = Some(ButtonOnClick {
594
2
            refany: data,
595
2
            callback: on_back.into(),
596
2
        })
597
2
        .into();
598
2
    }
599

            
600
    /// Builder method: sets the back callback and returns `self`.
601
    #[must_use]
602
2
    pub fn with_on_back<C: Into<super::button::ButtonOnClickCallback>>(
603
2
        mut self,
604
2
        data: RefAny,
605
2
        on_back: C,
606
2
    ) -> Self {
607
2
        self.set_on_back(data, on_back);
608
2
        self
609
2
    }
610

            
611
    /// Builder method: replaces the behavior set.
612
    #[must_use]
613
1
    pub const fn with_behavior(mut self, behavior: BackstageBehavior) -> Self {
614
1
        self.behavior = behavior;
615
1
        self
616
1
    }
617

            
618
    /// Builder method: replaces the style bundle.
619
    #[must_use]
620
    pub fn with_style(mut self, style: BackstageStyle) -> Self {
621
        self.style = style;
622
        self
623
    }
624

            
625
    /// Renders the backstage.
626
    #[must_use]
627
10
    pub fn dom(self) -> Dom {
628
        let Self {
629
10
            nav_items,
630
10
            active_item,
631
10
            on_nav_select,
632
10
            on_back,
633
10
            title_strip,
634
10
            content,
635
10
            behavior,
636
10
            style,
637
10
        } = self;
638

            
639
        // -- nav column --
640
10
        let mut nav_children: Vec<Dom> = Vec::with_capacity(nav_items.len() + 1);
641

            
642
10
        {
643
10
            let mut b = Button::create(AzString::from_const_str(""));
644
10
            b.icon = AzString::from_const_str("arrow_back");
645
10
            b.container_style = style.back_button_style.clone();
646
10
            b.icon_style = style.back_icon_style.clone();
647
10
            b.on_click = on_back.clone();
648
10
            nav_children.push(b.dom());
649
10
        }
650

            
651
92
        for (idx, item) in nav_items.into_library_owned_vec().into_iter().enumerate() {
652
92
            let (classes, mut part_style) = if idx == active_item {
653
10
                (
654
10
                    CLS_NAV_ITEM_ACTIVE,
655
10
                    merged_style(&style.nav_item_style, &style.nav_item_active_style),
656
10
                )
657
            } else {
658
82
                (CLS_NAV_ITEM, style.nav_item_style.clone())
659
            };
660
92
            if item.gap_before {
661
8
                part_style = merged_style(&part_style, &style.nav_item_gap_style);
662
84
            }
663
            // The nav item div is display:flex — a raw text run cannot be a
664
            // flex item (no anonymous-block wrapping in azul), so the label
665
            // gets its `<p>` per the label convention. Caught by `dom_lint`
666
            // on its very first run.
667
92
            let mut d = Dom::create_div()
668
92
                .with_ids_and_classes(IdOrClassVec::from_const_slice(classes))
669
92
                .with_css_props(part_style)
670
92
                .with_children(DomVec::from_vec(vec![Dom::create_p_with_text(item.label)]));
671
92
            if let Some(cb) = on_nav_select.as_ref() {
672
11
                d = d.with_callbacks(vec![CoreCallbackData {
673
11
                    event: EventFilter::Hover(HoverEventFilter::MouseUp),
674
11
                    callback: CoreCallback {
675
11
                        cb: on_backstage_nav_click as usize,
676
11
                        ctx: azul_core::refany::OptionRefAny::None,
677
11
                    },
678
11
                    refany: RefAny::new(NavClickData {
679
11
                        item_idx: idx,
680
11
                        on_nav_select: cb.clone(),
681
11
                    }),
682
11
                }].into());
683
81
            }
684
92
            nav_children.push(d);
685
        }
686

            
687
10
        let nav = Dom::create_div()
688
10
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_NAV))
689
10
            .with_css_props(style.nav_style.clone())
690
10
            .with_children(DomVec::from_vec(nav_children));
691

            
692
        // -- right side --
693
10
        let mut right_children: Vec<Dom> = Vec::with_capacity(2);
694
10
        if let Some(strip) = title_strip.into_option() {
695
1
            right_children.push(strip);
696
9
        }
697
10
        let pane = match content.into_option() {
698
            Some(c) => c,
699
10
            None => Dom::create_div(),
700
        };
701
10
        right_children.push(
702
10
            Dom::create_div()
703
10
                .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_CONTENT))
704
10
                .with_css_props(style.content_style.clone())
705
10
                .with_children(DomVec::from_vec(vec![pane])),
706
        );
707

            
708
10
        let right = Dom::create_div()
709
10
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_RIGHT))
710
10
            .with_css_props(style.right_style.clone())
711
10
            .with_children(DomVec::from_vec(right_children));
712

            
713
10
        let mut root = Dom::create_div()
714
10
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_BACKSTAGE))
715
10
            .with_css_props(style.root_style)
716
10
            .with_children(DomVec::from_vec(vec![nav, right]));
717

            
718
        // Escape leaves the backstage (window-level, focus-independent).
719
10
        if behavior.close_on_escape {
720
9
            if let Some(back) = on_back.into_option() {
721
1
                root = root.with_callbacks(vec![CoreCallbackData {
722
1
                    event: EventFilter::Window(WindowEventFilter::VirtualKeyDown),
723
1
                    callback: CoreCallback {
724
1
                        cb: on_backstage_key_down as usize,
725
1
                        ctx: azul_core::refany::OptionRefAny::None,
726
1
                    },
727
1
                    refany: RefAny::new(EscCloseData { on_back: back }),
728
1
                }].into());
729
8
            }
730
1
        }
731

            
732
10
        root
733
10
    }
734
}
735

            
736
impl Default for Backstage {
737
    fn default() -> Self {
738
        Self::office_2013()
739
    }
740
}
741

            
742
impl From<Backstage> for Dom {
743
    fn from(b: Backstage) -> Self {
744
        b.dom()
745
    }
746
}
747

            
748
18
fn merged_style(
749
18
    base: &CssPropertyWithConditionsVec,
750
18
    extra: &CssPropertyWithConditionsVec,
751
18
) -> CssPropertyWithConditionsVec {
752
18
    if extra.as_ref().is_empty() {
753
        return base.clone();
754
18
    }
755
18
    let mut v: Vec<Cond> = base.as_ref().to_vec();
756
18
    v.extend_from_slice(extra.as_ref());
757
18
    CssPropertyWithConditionsVec::from_vec(v)
758
18
}
759

            
760
// -- Nav-click / Escape plumbing --
761

            
762
/// Payload of one nav item: the item index plus the user's nav callback.
763
struct NavClickData {
764
    item_idx: usize,
765
    on_nav_select: BackstageOnNavSelect,
766
}
767

            
768
extern "C" fn on_backstage_nav_click(mut data: RefAny, info: CallbackInfo) -> Update {
769
    let Some(payload) = data.downcast_ref::<NavClickData>() else {
770
        return Update::DoNothing;
771
    };
772
    let idx = payload.item_idx;
773
    let cb = payload.on_nav_select.callback.cb;
774
    let refany = payload.on_nav_select.refany.clone();
775
    drop(payload);
776
    (cb)(refany, info, idx)
777
}
778

            
779
/// Payload of the window-level Escape handler: the user's back callback.
780
struct EscCloseData {
781
    on_back: ButtonOnClick,
782
}
783

            
784
extern "C" fn on_backstage_key_down(mut data: RefAny, info: CallbackInfo) -> Update {
785
    let Some(payload) = data.downcast_ref::<EscCloseData>() else {
786
        return Update::DoNothing;
787
    };
788
    let is_escape = matches!(
789
        info.get_current_keyboard_state().current_virtual_keycode.into_option(),
790
        Some(VirtualKeyCode::Escape)
791
    );
792
    if !is_escape {
793
        return Update::DoNothing;
794
    }
795
    let cb = payload.on_back.callback.cb;
796
    let refany = payload.on_back.refany.clone();
797
    drop(payload);
798
    (cb)(refany, info)
799
}
800

            
801
#[cfg(test)]
802
mod tests {
803
    use super::*;
804

            
805
    extern "C" fn nav_cb(_: RefAny, _: CallbackInfo, _: usize) -> Update {
806
        Update::DoNothing
807
    }
808

            
809
    extern "C" fn back_cb(_: RefAny, _: CallbackInfo) -> Update {
810
        Update::DoNothing
811
    }
812

            
813
    // ------------------------------------------------------------------
814
    // Constructors and invariants
815
    // ------------------------------------------------------------------
816

            
817
    #[test]
818
1
    fn backstage_office_2013_has_eleven_items_with_account_gapped() {
819
1
        let b = Backstage::office_2013();
820
1
        assert_eq!(b.nav_items.len(), 11);
821
1
        assert_eq!(b.active_item, 0);
822
1
        let items = b.nav_items.as_slice();
823
1
        assert_eq!(items[0].label.as_str(), "Info");
824
1
        assert_eq!(items[8].label.as_str(), "Close");
825
1
        assert_eq!(items[9].label.as_str(), "Account");
826
1
        assert!(items[9].gap_before);
827
1
        assert_eq!(items[10].label.as_str(), "Options");
828
1
        assert!(!items[10].gap_before);
829
1
    }
830

            
831
    #[test]
832
1
    fn backstage_style_default_is_office_2013() {
833
1
        assert_eq!(BackstageStyle::default(), BackstageStyle::office_2013());
834
1
    }
835

            
836
    #[test]
837
1
    fn backstage_behavior_default_closes_on_escape() {
838
1
        assert_eq!(BackstageBehavior::default(), BackstageBehavior::office_2013());
839
1
        assert!(BackstageBehavior::office_2013().close_on_escape);
840
1
        assert!(!BackstageBehavior::inert().close_on_escape);
841
1
    }
842

            
843
    // ------------------------------------------------------------------
844
    // DOM shape
845
    // ------------------------------------------------------------------
846

            
847
    #[test]
848
1
    fn dom_renders_nav_and_right_side() {
849
1
        let dom = Backstage::office_2013().dom();
850
1
        assert_eq!(dom.children.as_ref().len(), 2);
851
        // Nav: back button + 11 items.
852
1
        let nav = &dom.children.as_ref()[0];
853
1
        assert_eq!(nav.children.as_ref().len(), 12);
854
1
    }
855

            
856
    #[test]
857
1
    fn dom_places_the_title_strip_above_the_content() {
858
1
        let strip = Dom::create_div();
859
1
        let dom = Backstage::office_2013().with_title_strip(strip).dom();
860
1
        let right = &dom.children.as_ref()[1];
861
1
        assert_eq!(right.children.as_ref().len(), 2);
862
1
    }
863

            
864
    #[test]
865
1
    fn nav_items_get_click_callbacks_only_with_a_select_handler() {
866
        // Without a handler: inert items.
867
1
        let dom = Backstage::office_2013().dom();
868
1
        let nav = &dom.children.as_ref()[0];
869
11
        for item in nav.children.as_ref().iter().skip(1) {
870
11
            assert!(item.root.callbacks.as_ref().is_empty());
871
        }
872
        // With a handler: every item carries one.
873
1
        let dom = Backstage::office_2013()
874
1
            .with_on_nav_select(RefAny::new(()), nav_cb as BackstageOnNavSelectCallbackType)
875
1
            .dom();
876
1
        let nav = &dom.children.as_ref()[0];
877
11
        for item in nav.children.as_ref().iter().skip(1) {
878
11
            assert_eq!(item.root.callbacks.as_ref().len(), 1);
879
        }
880
1
    }
881

            
882
    #[test]
883
1
    fn escape_handler_is_attached_only_with_behavior_and_back_callback() {
884
        // Behavior on, no back callback: nothing to invoke, no handler.
885
1
        let dom = Backstage::office_2013().dom();
886
1
        assert!(dom.root.callbacks.as_ref().is_empty());
887
        // Behavior on + back callback: window-level key handler on the root.
888
1
        let dom = Backstage::office_2013()
889
1
            .with_on_back(RefAny::new(()), back_cb as super::super::button::ButtonOnClickCallbackType)
890
1
            .dom();
891
1
        assert_eq!(dom.root.callbacks.as_ref().len(), 1);
892
1
        assert_eq!(
893
1
            dom.root.callbacks.as_ref()[0].event,
894
            EventFilter::Window(WindowEventFilter::VirtualKeyDown)
895
        );
896
        // Behavior off: no handler even with a back callback.
897
1
        let dom = Backstage::office_2013()
898
1
            .with_on_back(RefAny::new(()), back_cb as super::super::button::ButtonOnClickCallbackType)
899
1
            .with_behavior(BackstageBehavior::inert())
900
1
            .dom();
901
1
        assert!(dom.root.callbacks.as_ref().is_empty());
902
1
    }
903

            
904
    #[test]
905
1
    fn active_item_gets_the_active_class() {
906
1
        let dom = Backstage::office_2013().with_active_item(2).dom();
907
1
        let nav = &dom.children.as_ref()[0];
908
        // Nav child 0 is the back button; item i is child i+1.
909
1
        let active = &nav.children.as_ref()[3];
910
1
        let classes = active.root.get_ids_and_classes();
911
2
        assert!(classes.as_ref().iter().any(|c| match c {
912
2
            Class(s) => s.as_str().contains("nav-item-active"),
913
            IdOrClass::Id(_) => false,
914
2
        }));
915
1
    }
916
}