1
//! Microsoft Office-style status bar widget (the Office-2013-era look look by default).
2
//!
3
//! Models the bottom chrome band of an Office document window:
4
//!
5
//! ```text
6
//! StatusBar ─ segments (left)             StatusBarSegment (icon? + label, clickable?)
7
//!           ─ filler
8
//!           ─ view switcher               StatusBarViewSwitcher (icon buttons, one active)
9
//!           ─ zoom cluster                StatusBarZoom (− button, slider, + button, "100%")
10
//! ```
11
//!
12
//! the Office-2013-era look reference (bottom of the window):
13
//! `PAGE 1 OF 1   0 WORDS   [spellcheck]  ENGLISH (UNITED STATES) …
14
//!  [read mode] [print layout] [web layout]   − ──────╂────── +  100%`
15
//!
16
//! Buttons are not re-implemented: the view-switcher buttons and the zoom
17
//! −/+ buttons expand to the existing [`super::button::Button`] widget with
18
//! status-bar part styles injected through `Button`'s public style fields
19
//! (the same composition rule the ribbon uses). The zoom slider embeds the
20
//! existing [`super::slider::Slider`] widget, restyled through its public
21
//! `track_style` / `thumb_style` fields, so dragging it reports values
22
//! through the regular [`super::slider::SliderOnValueChange`] callback.
23
//!
24
//! All visual parts are exposed on [`StatusBarStyle`] (defaults = the Office-2013-era look
25
//! look, [`StatusBarStyle::office_2013`]); replace any field to re-theme
26
//! without touching widget code. There is no behavior struct: the status bar
27
//! has no self-driven chrome interactions — every event is forwarded to the
28
//! application callbacks.
29

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

            
47
use azul_css::{impl_option, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_mut};
48

            
49
use crate::callbacks::CallbackInfo;
50

            
51
use super::{
52
    button::{Button, OptionButtonOnClick},
53
    slider::{OptionSliderOnValueChange, Slider},
54
};
55

            
56
// -- Callbacks --
57

            
58
/// Callback signature invoked when a view-switcher button is clicked
59
/// (receives the view index).
60
pub type StatusBarOnViewSelectCallbackType =
61
    extern "C" fn(RefAny, CallbackInfo, usize) -> Update;
62
impl_widget_callback!(
63
    StatusBarOnViewSelect, OptionStatusBarOnViewSelect,
64
    StatusBarOnViewSelectCallback, StatusBarOnViewSelectCallbackType
65
);
66

            
67
azul_core::impl_managed_callback! {
68
    wrapper:        StatusBarOnViewSelectCallback,
69
    info_ty:        CallbackInfo,
70
    return_ty:      Update,
71
    default_ret:    Update::DoNothing,
72
    invoker_static: STATUS_BAR_ON_VIEW_SELECT_INVOKER,
73
    invoker_ty:     AzStatusBarOnViewSelectCallbackInvoker,
74
    thunk_fn:       az_status_bar_on_view_select_callback_thunk,
75
    setter_fn:      AzApp_setStatusBarOnViewSelectCallbackInvoker,
76
    from_handle_fn: AzStatusBarOnViewSelectCallback_createFromHostHandle,
77
    extra_args:     [ view_index: usize ],
78
}
79

            
80
// -- Font --
81

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

            
87
// -- the Office-2013-era look palette (seeds StatusBarTheme::office_2013) --
88

            
89
const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
90
const TRANSPARENT: ColorU = ColorU { r: 0, g: 0, b: 0, a: 0 };
91
/// Office 2013 accent blue (#2B579A): the bar fill.
92
const W13_BLUE: ColorU = ColorU { r: 43, g: 87, b: 154, a: 255 };
93
/// Hover fill on bar controls (lighter blue, #3E6DB5).
94
const W13_BAR_HOVER: ColorU = ColorU { r: 62, g: 109, b: 181, a: 255 };
95
/// Pressed / active-view fill (darker blue, #1E3E6F).
96
const W13_BAR_PRESSED: ColorU = ColorU { r: 30, g: 62, b: 111, a: 255 };
97
/// Zoom slider rail (semi-light blue-gray, #A5BDDE).
98
const W13_RAIL: ColorU = ColorU { r: 165, g: 189, b: 222, a: 255 };
99
/// Zoom slider thumb border (#8E9BB3).
100
const W13_THUMB_BORDER: ColorU = ColorU { r: 142, g: 155, b: 179, a: 255 };
101

            
102
// -- Metrics (the Office-2013-era look, logical px) --
103

            
104
/// Bar height.
105
const BAR_HEIGHT: isize = 23;
106
/// Status text size.
107
const TEXT_PX: isize = 11;
108
/// View-switcher / zoom glyph size.
109
const ICON_PX: isize = 15;
110
/// Width of one view-switcher button.
111
const VIEW_BUTTON_W: isize = 29;
112
/// Width of the zoom −/+ buttons.
113
const ZOOM_BUTTON_W: isize = 21;
114
/// Total width of the zoom slider track.
115
const ZOOM_TRACK_W: isize = 100;
116
/// Zoom thumb size (office-2013: a small vertical bar).
117
const ZOOM_THUMB_W: isize = 6;
118
const ZOOM_THUMB_H: isize = 11;
119
/// Width reserved for the "100%" label.
120
const ZOOM_LABEL_W: isize = 42;
121

            
122
// -- Theme --
123

            
124
/// Color palette from which a full [`StatusBarStyle`] is derived via
125
/// [`StatusBarStyle::from_theme`]. All fields are plain colors, so themes
126
/// are trivially constructible over FFI. Preset: [`StatusBarTheme::office_2013`]
127
/// (the default).
128
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
129
#[repr(C)]
130
pub struct StatusBarTheme {
131
    /// Bar fill (office-2013: accent blue).
132
    pub bar_bg: ColorU,
133
    /// Text and glyph color on the bar.
134
    pub text: ColorU,
135
    /// Hover fill on clickable bar controls.
136
    pub hover_bg: ColorU,
137
    /// Pressed fill on bar controls.
138
    pub pressed_bg: ColorU,
139
    /// Fill of the active view-switcher button.
140
    pub view_active_bg: ColorU,
141
    /// Zoom slider rail line.
142
    pub rail: ColorU,
143
    /// Zoom slider thumb fill.
144
    pub thumb: ColorU,
145
    /// Zoom slider thumb border.
146
    pub thumb_border: ColorU,
147
}
148

            
149
impl StatusBarTheme {
150
    /// The the Office-2013-era look palette: #2B579A bar, white text, lighter-blue hovers.
151
    #[must_use]
152
16
    pub const fn office_2013() -> Self {
153
16
        Self {
154
16
            bar_bg: W13_BLUE,
155
16
            text: WHITE,
156
16
            hover_bg: W13_BAR_HOVER,
157
16
            pressed_bg: W13_BAR_PRESSED,
158
16
            view_active_bg: W13_BAR_PRESSED,
159
16
            rail: W13_RAIL,
160
16
            thumb: WHITE,
161
16
            thumb_border: W13_THUMB_BORDER,
162
16
        }
163
16
    }
164
}
165

            
166
impl Default for StatusBarTheme {
167
    fn default() -> Self {
168
        Self::office_2013()
169
    }
170
}
171

            
172
// -- Theme -> property-list builders --
173
//
174
// Every themed status-bar part is built from `StatusBarTheme` colors by the
175
// functions below; `StatusBarStyle::office_2013()` is just
176
// `from_theme(StatusBarTheme::office_2013())`, so there is exactly one source
177
// of truth for each part's property list.
178

            
179
288
fn bg_vec(c: ColorU) -> StyleBackgroundContentVec {
180
288
    StyleBackgroundContentVec::from_vec(vec![StyleBackgroundContent::Color(c)])
181
288
}
182

            
183
160
fn cond_bg(c: ColorU) -> Cond {
184
160
    Cond::simple(P::const_background_content(bg_vec(c)))
185
160
}
186

            
187
64
fn cond_bg_hover(c: ColorU) -> Cond {
188
64
    Cond::on_hover(P::const_background_content(bg_vec(c)))
189
64
}
190

            
191
64
fn cond_bg_active(c: ColorU) -> Cond {
192
64
    Cond::on_active(P::const_background_content(bg_vec(c)))
193
64
}
194

            
195
96
const fn cond_text_color(c: ColorU) -> Cond {
196
96
    Cond::simple(P::const_text_color(StyleTextColor { inner: c }))
197
96
}
198

            
199
128
const fn cond_border_box() -> Cond {
200
128
    Cond::simple(P::const_box_sizing(LayoutBoxSizing::BorderBox))
201
128
}
202

            
203
112
fn push_row_center(v: &mut Vec<Cond>) {
204
112
    v.push(Cond::simple(P::const_display(LayoutDisplay::Flex)));
205
112
    v.push(Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)));
206
112
    v.push(Cond::simple(P::const_align_items(LayoutAlignItems::Center)));
207
112
    v.push(Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))));
208
112
    v.push(Cond::simple(P::const_flex_shrink(LayoutFlexShrink { inner: FloatValue::const_new(0) })));
209
112
}
210

            
211
/// Transparent, hover-highlighted flat button chassis shared by every
212
/// clickable status-bar control. The explicit TRANSPARENT border overrides
213
/// the [`Button`] widget's default frame (the bar's controls are flat).
214
64
fn push_flat_button(v: &mut Vec<Cond>, t: &StatusBarTheme) {
215
64
    v.push(cond_border_box());
216
64
    v.push(Cond::simple(P::const_cursor(StyleCursor::Default)));
217
64
    v.push(Cond::simple(P::user_select(StyleUserSelect::None)));
218
64
    v.push(cond_bg(TRANSPARENT));
219
64
    push_box_border(v, TRANSPARENT);
220
64
    v.push(cond_bg_hover(t.hover_bg));
221
64
    v.push(cond_bg_active(t.pressed_bg));
222
64
}
223

            
224
/// 1px solid border on all four sides in the given color.
225
64
fn push_box_border(v: &mut Vec<Cond>, c: ColorU) {
226
64
    v.push(Cond::simple(P::const_border_top_width(LayoutBorderTopWidth::const_px(1))));
227
64
    v.push(Cond::simple(P::const_border_left_width(LayoutBorderLeftWidth::const_px(1))));
228
64
    v.push(Cond::simple(P::const_border_right_width(LayoutBorderRightWidth::const_px(1))));
229
64
    v.push(Cond::simple(P::const_border_bottom_width(LayoutBorderBottomWidth::const_px(1))));
230
64
    v.push(Cond::simple(P::const_border_top_style(StyleBorderTopStyle { inner: BorderStyle::Solid })));
231
64
    v.push(Cond::simple(P::const_border_left_style(StyleBorderLeftStyle { inner: BorderStyle::Solid })));
232
64
    v.push(Cond::simple(P::const_border_right_style(StyleBorderRightStyle { inner: BorderStyle::Solid })));
233
64
    v.push(Cond::simple(P::const_border_bottom_style(StyleBorderBottomStyle { inner: BorderStyle::Solid })));
234
64
    v.push(Cond::simple(P::const_border_top_color(StyleBorderTopColor { inner: c })));
235
64
    v.push(Cond::simple(P::const_border_left_color(StyleBorderLeftColor { inner: c })));
236
64
    v.push(Cond::simple(P::const_border_right_color(StyleBorderRightColor { inner: c })));
237
64
    v.push(Cond::simple(P::const_border_bottom_color(StyleBorderBottomColor { inner: c })));
238
64
}
239

            
240
16
fn theme_bar(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
241
16
    let mut v = Vec::new();
242
16
    push_row_center(&mut v);
243
16
    v.push(cond_border_box());
244
16
    v.push(Cond::simple(P::const_height(LayoutHeight::const_px(BAR_HEIGHT))));
245
16
    v.push(Cond::simple(P::const_font_family(SYSTEM_UI_FAMILY)));
246
16
    v.push(Cond::simple(P::const_font_size(StyleFontSize::const_px(TEXT_PX))));
247
16
    v.push(cond_bg(t.bar_bg));
248
16
    v.push(cond_text_color(t.text));
249
16
    v.push(Cond::simple(P::const_padding_left(LayoutPaddingLeft::const_px(6))));
250
16
    v.push(Cond::simple(P::const_padding_right(LayoutPaddingRight::const_px(4))));
251
16
    CssPropertyWithConditionsVec::from_vec(v)
252
16
}
253

            
254
16
fn theme_segment(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
255
16
    let mut v = Vec::new();
256
16
    push_row_center(&mut v);
257
16
    push_flat_button(&mut v, t);
258
16
    v.push(Cond::simple(P::const_height(LayoutHeight::const_px(BAR_HEIGHT))));
259
16
    v.push(Cond::simple(P::const_padding_left(LayoutPaddingLeft::const_px(7))));
260
16
    v.push(Cond::simple(P::const_padding_right(LayoutPaddingRight::const_px(7))));
261
16
    CssPropertyWithConditionsVec::from_vec(v)
262
16
}
263

            
264
16
fn theme_segment_icon(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
265
16
    CssPropertyWithConditionsVec::from_vec(vec![
266
16
        Cond::simple(P::const_font_size(StyleFontSize::const_px(ICON_PX - 1))),
267
16
        cond_text_color(t.text),
268
    ])
269
16
}
270

            
271
16
fn theme_segment_label(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
272
16
    CssPropertyWithConditionsVec::from_vec(vec![
273
16
        Cond::simple(P::const_font_size(StyleFontSize::const_px(TEXT_PX))),
274
16
        cond_text_color(t.text),
275
    ])
276
16
}
277

            
278
16
fn theme_filler(_t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
279
16
    CssPropertyWithConditionsVec::from_vec(vec![
280
16
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
281
    ])
282
16
}
283

            
284
16
fn theme_views(_t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
285
16
    let mut v = Vec::new();
286
16
    push_row_center(&mut v);
287
16
    v.push(Cond::simple(P::const_margin_right(LayoutMarginRight::const_px(6))));
288
16
    CssPropertyWithConditionsVec::from_vec(v)
289
16
}
290

            
291
16
fn theme_view_button(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
292
16
    let mut v = Vec::new();
293
16
    push_row_center(&mut v);
294
16
    push_flat_button(&mut v, t);
295
16
    v.push(Cond::simple(P::const_justify_content(LayoutJustifyContent::Center)));
296
16
    v.push(Cond::simple(P::const_width(LayoutWidth::const_px(VIEW_BUTTON_W))));
297
16
    v.push(Cond::simple(P::const_height(LayoutHeight::const_px(BAR_HEIGHT))));
298
16
    CssPropertyWithConditionsVec::from_vec(v)
299
16
}
300

            
301
/// APPENDED to the active view-switcher button.
302
16
fn theme_view_button_active(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
303
16
    CssPropertyWithConditionsVec::from_vec(vec![cond_bg(t.view_active_bg)])
304
16
}
305

            
306
16
fn theme_view_icon(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
307
16
    CssPropertyWithConditionsVec::from_vec(vec![
308
16
        Cond::simple(P::const_font_size(StyleFontSize::const_px(ICON_PX))),
309
16
        cond_text_color(t.text),
310
    ])
311
16
}
312

            
313
16
fn theme_zoom(_t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
314
16
    let mut v = Vec::new();
315
16
    push_row_center(&mut v);
316
16
    CssPropertyWithConditionsVec::from_vec(v)
317
16
}
318

            
319
16
fn theme_zoom_button(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
320
16
    let mut v = Vec::new();
321
16
    push_row_center(&mut v);
322
16
    push_flat_button(&mut v, t);
323
16
    v.push(Cond::simple(P::const_justify_content(LayoutJustifyContent::Center)));
324
16
    v.push(Cond::simple(P::const_width(LayoutWidth::const_px(ZOOM_BUTTON_W))));
325
16
    v.push(Cond::simple(P::const_height(LayoutHeight::const_px(BAR_HEIGHT))));
326
16
    CssPropertyWithConditionsVec::from_vec(v)
327
16
}
328

            
329
16
fn theme_zoom_icon(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
330
16
    CssPropertyWithConditionsVec::from_vec(vec![
331
16
        Cond::simple(P::const_font_size(StyleFontSize::const_px(ICON_PX - 2))),
332
16
        cond_text_color(t.text),
333
    ])
334
16
}
335

            
336
/// Positioning context for the rail line, the center tick and the slider.
337
16
fn theme_zoom_track_host(_t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
338
16
    CssPropertyWithConditionsVec::from_vec(vec![
339
16
        cond_border_box(),
340
16
        Cond::simple(P::const_position(LayoutPosition::Relative)),
341
16
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
342
16
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
343
16
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
344
16
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
345
16
        Cond::simple(P::const_width(LayoutWidth::const_px(ZOOM_TRACK_W))),
346
16
        Cond::simple(P::const_height(LayoutHeight::const_px(BAR_HEIGHT))),
347
    ])
348
16
}
349

            
350
/// The 1px horizontal rail line behind the slider.
351
16
fn theme_zoom_rail(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
352
16
    CssPropertyWithConditionsVec::from_vec(vec![
353
16
        Cond::simple(P::const_position(LayoutPosition::Absolute)),
354
16
        Cond::simple(P::const_left(LayoutLeft::const_px(0))),
355
16
        Cond::simple(P::const_top(LayoutTop::const_px(BAR_HEIGHT / 2))),
356
16
        Cond::simple(P::const_width(LayoutWidth::const_px(ZOOM_TRACK_W))),
357
16
        Cond::simple(P::const_height(LayoutHeight::const_px(1))),
358
16
        cond_bg(t.rail),
359
    ])
360
16
}
361

            
362
/// The small vertical tick marking the 100% center of the rail.
363
16
fn theme_zoom_tick(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
364
16
    CssPropertyWithConditionsVec::from_vec(vec![
365
16
        Cond::simple(P::const_position(LayoutPosition::Absolute)),
366
16
        Cond::simple(P::const_left(LayoutLeft::const_px(ZOOM_TRACK_W / 2))),
367
16
        Cond::simple(P::const_top(LayoutTop::const_px(BAR_HEIGHT / 2 - 3))),
368
16
        Cond::simple(P::const_width(LayoutWidth::const_px(1))),
369
16
        Cond::simple(P::const_height(LayoutHeight::const_px(7))),
370
16
        cond_bg(t.rail),
371
    ])
372
16
}
373

            
374
/// Injected into the embedded [`Slider`]'s `track_style`: a transparent
375
/// full-height hit area (the visible rail is drawn by the host).
376
16
fn theme_slider_track(_t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
377
16
    CssPropertyWithConditionsVec::from_vec(vec![
378
16
        cond_border_box(),
379
16
        Cond::simple(P::const_display(LayoutDisplay::Flex)),
380
16
        Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
381
16
        Cond::simple(P::const_align_items(LayoutAlignItems::Center)),
382
16
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
383
16
        Cond::simple(P::const_width(LayoutWidth::const_px(ZOOM_TRACK_W))),
384
16
        Cond::simple(P::const_height(LayoutHeight::const_px(BAR_HEIGHT))),
385
16
        cond_bg(TRANSPARENT),
386
16
        Cond::simple(P::const_cursor(StyleCursor::Default)),
387
    ])
388
16
}
389

            
390
/// Injected into the embedded [`Slider`]'s `thumb_style` (position-independent
391
/// part; `StatusBar::dom` appends the `margin-left` computed from the zoom
392
/// percent).
393
16
fn theme_slider_thumb(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
394
16
    let mut v = vec![
395
16
        cond_border_box(),
396
16
        Cond::simple(P::const_width(LayoutWidth::const_px(ZOOM_THUMB_W))),
397
16
        Cond::simple(P::const_height(LayoutHeight::const_px(ZOOM_THUMB_H))),
398
16
        Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(0))),
399
16
        cond_bg(t.thumb),
400
    ];
401
16
    v.push(Cond::simple(P::const_border_top_width(LayoutBorderTopWidth::const_px(1))));
402
16
    v.push(Cond::simple(P::const_border_left_width(LayoutBorderLeftWidth::const_px(1))));
403
16
    v.push(Cond::simple(P::const_border_right_width(LayoutBorderRightWidth::const_px(1))));
404
16
    v.push(Cond::simple(P::const_border_bottom_width(LayoutBorderBottomWidth::const_px(1))));
405
16
    v.push(Cond::simple(P::const_border_top_style(StyleBorderTopStyle { inner: BorderStyle::Solid })));
406
16
    v.push(Cond::simple(P::const_border_left_style(StyleBorderLeftStyle { inner: BorderStyle::Solid })));
407
16
    v.push(Cond::simple(P::const_border_right_style(StyleBorderRightStyle { inner: BorderStyle::Solid })));
408
16
    v.push(Cond::simple(P::const_border_bottom_style(StyleBorderBottomStyle { inner: BorderStyle::Solid })));
409
16
    v.push(Cond::simple(P::const_border_top_color(StyleBorderTopColor { inner: t.thumb_border })));
410
16
    v.push(Cond::simple(P::const_border_left_color(StyleBorderLeftColor { inner: t.thumb_border })));
411
16
    v.push(Cond::simple(P::const_border_right_color(StyleBorderRightColor { inner: t.thumb_border })));
412
16
    v.push(Cond::simple(P::const_border_bottom_color(StyleBorderBottomColor { inner: t.thumb_border })));
413
16
    CssPropertyWithConditionsVec::from_vec(v)
414
16
}
415

            
416
16
fn theme_zoom_label(t: &StatusBarTheme) -> CssPropertyWithConditionsVec {
417
16
    let mut v = Vec::new();
418
16
    push_row_center(&mut v);
419
16
    push_flat_button(&mut v, t);
420
16
    v.push(Cond::simple(P::const_justify_content(LayoutJustifyContent::End)));
421
16
    v.push(Cond::simple(P::const_width(LayoutWidth::const_px(ZOOM_LABEL_W))));
422
16
    v.push(Cond::simple(P::const_height(LayoutHeight::const_px(BAR_HEIGHT))));
423
16
    v.push(Cond::simple(P::const_padding_right(LayoutPaddingRight::const_px(6))));
424
16
    v.push(Cond::simple(P::const_font_size(StyleFontSize::const_px(TEXT_PX))));
425
16
    v.push(cond_text_color(t.text));
426
16
    CssPropertyWithConditionsVec::from_vec(v)
427
16
}
428

            
429
// -- Style --
430

            
431
/// All part styles of the status bar. Every part defaults to the the Office-2013-era look
432
/// look; replace any field for finer control (the same override API as
433
/// [`super::ribbon::RibbonStyle`]).
434
#[derive(Debug, Clone, PartialEq, Eq)]
435
#[repr(C)]
436
pub struct StatusBarStyle {
437
    /// The palette this style bundle was derived from. Kept for consumers
438
    /// deriving matching custom parts.
439
    pub theme: StatusBarTheme,
440
    /// The bar itself (horizontal row).
441
    pub bar_style: CssPropertyWithConditionsVec,
442
    /// One left-hand segment (page count, word count, language, …).
443
    pub segment_style: CssPropertyWithConditionsVec,
444
    /// Icon inside a segment.
445
    pub segment_icon_style: CssPropertyWithConditionsVec,
446
    /// Text label inside a segment.
447
    pub segment_label_style: CssPropertyWithConditionsVec,
448
    /// Flexible spacer between the left segments and the right clusters.
449
    pub filler_style: CssPropertyWithConditionsVec,
450
    /// The view-switcher cluster.
451
    pub views_style: CssPropertyWithConditionsVec,
452
    /// Container style injected into one view-switcher [`Button`].
453
    pub view_button_style: CssPropertyWithConditionsVec,
454
    /// APPENDED to the active view-switcher button.
455
    pub view_button_active_style: CssPropertyWithConditionsVec,
456
    /// Icon style injected into the view-switcher [`Button`]s.
457
    pub view_icon_style: CssPropertyWithConditionsVec,
458
    /// The zoom cluster (− slider + label).
459
    pub zoom_style: CssPropertyWithConditionsVec,
460
    /// Container style injected into the zoom −/+ [`Button`]s.
461
    pub zoom_button_style: CssPropertyWithConditionsVec,
462
    /// Icon style injected into the zoom −/+ [`Button`]s.
463
    pub zoom_icon_style: CssPropertyWithConditionsVec,
464
    /// Positioning host around the rail, tick and embedded slider.
465
    pub zoom_track_host_style: CssPropertyWithConditionsVec,
466
    /// The 1px rail line.
467
    pub zoom_rail_style: CssPropertyWithConditionsVec,
468
    /// The center (100%) tick on the rail.
469
    pub zoom_tick_style: CssPropertyWithConditionsVec,
470
    /// Track style injected into the embedded [`Slider`].
471
    pub slider_track_style: CssPropertyWithConditionsVec,
472
    /// Thumb style injected into the embedded [`Slider`] (without the
473
    /// position; `dom()` appends the computed `margin-left`).
474
    pub slider_thumb_style: CssPropertyWithConditionsVec,
475
    /// The "100%" zoom percent label.
476
    pub zoom_label_style: CssPropertyWithConditionsVec,
477
}
478

            
479
impl StatusBarStyle {
480
    /// The the Office-2013-era look look (#2B579A bar, white text) - the default.
481
    #[must_use]
482
16
    pub fn office_2013() -> Self {
483
16
        Self::from_theme(StatusBarTheme::office_2013())
484
16
    }
485

            
486
    /// Derives every part style from the given palette.
487
    #[must_use]
488
16
    pub fn from_theme(theme: StatusBarTheme) -> Self {
489
16
        let t = &theme;
490
16
        Self {
491
16
            theme,
492
16
            bar_style: theme_bar(t),
493
16
            segment_style: theme_segment(t),
494
16
            segment_icon_style: theme_segment_icon(t),
495
16
            segment_label_style: theme_segment_label(t),
496
16
            filler_style: theme_filler(t),
497
16
            views_style: theme_views(t),
498
16
            view_button_style: theme_view_button(t),
499
16
            view_button_active_style: theme_view_button_active(t),
500
16
            view_icon_style: theme_view_icon(t),
501
16
            zoom_style: theme_zoom(t),
502
16
            zoom_button_style: theme_zoom_button(t),
503
16
            zoom_icon_style: theme_zoom_icon(t),
504
16
            zoom_track_host_style: theme_zoom_track_host(t),
505
16
            zoom_rail_style: theme_zoom_rail(t),
506
16
            zoom_tick_style: theme_zoom_tick(t),
507
16
            slider_track_style: theme_slider_track(t),
508
16
            slider_thumb_style: theme_slider_thumb(t),
509
16
            zoom_label_style: theme_zoom_label(t),
510
16
        }
511
16
    }
512
}
513

            
514
impl Default for StatusBarStyle {
515
1
    fn default() -> Self {
516
1
        Self::office_2013()
517
1
    }
518
}
519

            
520
// -- Data model --
521

            
522
/// One left-hand status segment ("PAGE 1 OF 1", "0 WORDS", the language, …).
523
///
524
/// An empty `icon` means "no icon". With an `on_click` the segment renders
525
/// as a flat hover-highlighted button (office-2013: the segments open panes /
526
/// toggle counters); without one it is inert text.
527
#[derive(Debug, Clone, PartialEq, Eq)]
528
#[repr(C)]
529
pub struct StatusBarSegment {
530
    /// Optional leading icon name, resolved through the registered icon
531
    /// provider (e.g. the builtin Material Icons pack: "spellcheck").
532
    pub icon: AzString,
533
    /// The segment text.
534
    pub label: AzString,
535
    /// Optional click handler.
536
    pub on_click: OptionButtonOnClick,
537
}
538

            
539
impl StatusBarSegment {
540
    /// Creates a text-only, inert segment.
541
    #[must_use]
542
11
    pub fn new(label: AzString) -> Self {
543
11
        Self {
544
11
            icon: AzString::from_const_str(""),
545
11
            label,
546
11
            on_click: None.into(),
547
11
        }
548
11
    }
549

            
550
    /// Builder method: sets the leading icon name.
551
    #[must_use]
552
    pub fn with_icon(mut self, icon: AzString) -> Self {
553
        self.icon = icon;
554
        self
555
    }
556

            
557
    /// Sets the click callback.
558
    pub fn set_on_click<C: Into<super::button::ButtonOnClickCallback>>(
559
        &mut self,
560
        data: RefAny,
561
        on_click: C,
562
    ) {
563
        self.on_click = Some(super::button::ButtonOnClick {
564
            refany: data,
565
            callback: on_click.into(),
566
        })
567
        .into();
568
    }
569

            
570
    /// Builder method: sets the click callback and returns `self`.
571
    #[must_use]
572
    pub fn with_on_click<C: Into<super::button::ButtonOnClickCallback>>(
573
        mut self,
574
        data: RefAny,
575
        on_click: C,
576
    ) -> Self {
577
        self.set_on_click(data, on_click);
578
        self
579
    }
580
}
581

            
582
impl_option!(
583
    StatusBarSegment,
584
    OptionStatusBarSegment,
585
    copy = false,
586
    [Debug, Clone, PartialEq]
587
);
588
impl_vec!(
589
    StatusBarSegment,
590
    StatusBarSegmentVec,
591
    StatusBarSegmentVecDestructor,
592
    StatusBarSegmentVecDestructorType,
593
    StatusBarSegmentVecSlice,
594
    OptionStatusBarSegment
595
);
596
impl_vec_clone!(StatusBarSegment, StatusBarSegmentVec, StatusBarSegmentVecDestructor);
597
impl_vec_debug!(StatusBarSegment, StatusBarSegmentVec);
598
impl_vec_mut!(StatusBarSegment, StatusBarSegmentVec);
599

            
600
/// One view-switcher button (office-2013: read mode / print layout / web layout).
601
#[derive(Debug, Clone, PartialEq, Eq)]
602
#[repr(C)]
603
pub struct StatusBarView {
604
    /// Icon name, resolved through the registered icon provider.
605
    pub icon: AzString,
606
}
607

            
608
impl StatusBarView {
609
    /// Creates a view button with the given icon name.
610
    #[must_use]
611
12
    pub const fn new(icon: AzString) -> Self {
612
12
        Self { icon }
613
12
    }
614
}
615

            
616
impl_option!(
617
    StatusBarView,
618
    OptionStatusBarView,
619
    copy = false,
620
    [Debug, Clone, PartialEq]
621
);
622
impl_vec!(
623
    StatusBarView,
624
    StatusBarViewVec,
625
    StatusBarViewVecDestructor,
626
    StatusBarViewVecDestructorType,
627
    StatusBarViewVecSlice,
628
    OptionStatusBarView
629
);
630
impl_vec_clone!(StatusBarView, StatusBarViewVec, StatusBarViewVecDestructor);
631
impl_vec_debug!(StatusBarView, StatusBarViewVec);
632
impl_vec_mut!(StatusBarView, StatusBarViewVec);
633

            
634
/// The view-switcher cluster: a row of icon buttons with one active view.
635
#[derive(Debug, Clone)]
636
#[repr(C)]
637
pub struct StatusBarViewSwitcher {
638
    /// The view buttons, left to right.
639
    pub views: StatusBarViewVec,
640
    /// Index of the active view (highlighted).
641
    pub active_view: usize,
642
    /// Optional callback fired when a view is clicked (receives the index).
643
    pub on_select: OptionStatusBarOnViewSelect,
644
}
645

            
646
impl StatusBarViewSwitcher {
647
    /// Creates a view switcher with the given views; view 0 is active.
648
    #[must_use]
649
    pub fn new(views: StatusBarViewVec) -> Self {
650
        Self { views, active_view: 0, on_select: None.into() }
651
    }
652

            
653
    /// The the Office-2013-era look trio: read mode, print layout (active), web layout.
654
    #[must_use]
655
4
    pub fn office_2013() -> Self {
656
4
        let views = StatusBarViewVec::from_vec(vec![
657
4
            StatusBarView::new(AzString::from_const_str("menu_book")),
658
4
            StatusBarView::new(AzString::from_const_str("description")),
659
4
            StatusBarView::new(AzString::from_const_str("public")),
660
        ]);
661
4
        Self { views, active_view: 1, on_select: None.into() }
662
4
    }
663

            
664
    /// Builder method: sets the active view index.
665
    #[must_use]
666
    pub const fn with_active_view(mut self, active_view: usize) -> Self {
667
        self.active_view = active_view;
668
        self
669
    }
670

            
671
    /// Sets the view-select callback.
672
1
    pub fn set_on_select<C: Into<StatusBarOnViewSelectCallback>>(
673
1
        &mut self,
674
1
        data: RefAny,
675
1
        on_select: C,
676
1
    ) {
677
1
        self.on_select = Some(StatusBarOnViewSelect {
678
1
            refany: data,
679
1
            callback: on_select.into(),
680
1
        })
681
1
        .into();
682
1
    }
683

            
684
    /// Builder method: sets the view-select callback and returns `self`.
685
    #[must_use]
686
    pub fn with_on_select<C: Into<StatusBarOnViewSelectCallback>>(
687
        mut self,
688
        data: RefAny,
689
        on_select: C,
690
    ) -> Self {
691
        self.set_on_select(data, on_select);
692
        self
693
    }
694
}
695

            
696
impl_option!(
697
    StatusBarViewSwitcher,
698
    OptionStatusBarViewSwitcher,
699
    copy = false,
700
    [Debug, Clone]
701
);
702

            
703
/// The zoom cluster: − button, slider, + button and the percent label.
704
///
705
/// The slider is the existing [`Slider`] widget with `value = percent` over
706
/// the linear `[min, max]` window. The the Office-2013-era look default window is
707
/// `[10, 190]` so the 100% default rests exactly on the center tick (the
708
/// original office-suite slider maps 10–100–500 piecewise; a linear window keeps the
709
/// widget dumb — the application decides what a slider value means).
710
#[derive(Debug, Clone, PartialEq)]
711
#[repr(C)]
712
pub struct StatusBarZoom {
713
    /// Current zoom percent, shown in the label and placing the thumb.
714
    pub percent: f32,
715
    /// Slider window minimum (thumb far left).
716
    pub min: f32,
717
    /// Slider window maximum (thumb far right).
718
    pub max: f32,
719
    /// Optional − button callback.
720
    pub on_zoom_out: OptionButtonOnClick,
721
    /// Optional + button callback.
722
    pub on_zoom_in: OptionButtonOnClick,
723
    /// Optional slider drag/press callback (reports the raw slider value).
724
    pub on_slider_change: OptionSliderOnValueChange,
725
    /// Renders the "100%" label after the + button.
726
    pub show_label: bool,
727
}
728

            
729
impl StatusBarZoom {
730
    /// 100% zoom over the the Office-2013-era look `[10, 190]` window, label shown.
731
    #[must_use]
732
4
    pub fn office_2013() -> Self {
733
4
        Self {
734
4
            percent: 100.0,
735
4
            min: 10.0,
736
4
            max: 190.0,
737
4
            on_zoom_out: None.into(),
738
4
            on_zoom_in: None.into(),
739
4
            on_slider_change: None.into(),
740
4
            show_label: true,
741
4
        }
742
4
    }
743

            
744
    /// Builder method: sets the zoom percent.
745
    #[must_use]
746
    pub const fn with_percent(mut self, percent: f32) -> Self {
747
        self.percent = percent;
748
        self
749
    }
750
}
751

            
752
impl Default for StatusBarZoom {
753
    fn default() -> Self {
754
        Self::office_2013()
755
    }
756
}
757

            
758
impl_option!(
759
    StatusBarZoom,
760
    OptionStatusBarZoom,
761
    copy = false,
762
    [Debug, Clone, PartialEq]
763
);
764

            
765
/// Top-level status bar widget: left segments, a filler, then the
766
/// view switcher and zoom cluster on the right.
767
#[derive(Debug, Clone)]
768
#[repr(C)]
769
pub struct StatusBar {
770
    /// Left-hand segments, in order.
771
    pub segments: StatusBarSegmentVec,
772
    /// Optional view-switcher cluster.
773
    pub views: OptionStatusBarViewSwitcher,
774
    /// Optional zoom cluster.
775
    pub zoom: OptionStatusBarZoom,
776
    /// All part styles (defaults to the the Office-2013-era look look).
777
    pub style: StatusBarStyle,
778
}
779

            
780
// -- CSS classes --
781

            
782
static CLS_STATUSBAR: &[IdOrClass] =
783
    &[Class(AzString::from_const_str("__azul-native-statusbar"))];
784
static CLS_SEGMENT: &[IdOrClass] =
785
    &[Class(AzString::from_const_str("__azul-native-statusbar-segment"))];
786
static CLS_FILLER: &[IdOrClass] =
787
    &[Class(AzString::from_const_str("__azul-native-statusbar-filler"))];
788
static CLS_VIEWS: &[IdOrClass] =
789
    &[Class(AzString::from_const_str("__azul-native-statusbar-views"))];
790
static CLS_ZOOM: &[IdOrClass] =
791
    &[Class(AzString::from_const_str("__azul-native-statusbar-zoom"))];
792
static CLS_ZOOM_TRACK: &[IdOrClass] =
793
    &[Class(AzString::from_const_str("__azul-native-statusbar-zoom-track"))];
794
static CLS_ZOOM_RAIL: &[IdOrClass] =
795
    &[Class(AzString::from_const_str("__azul-native-statusbar-zoom-rail"))];
796
static CLS_ZOOM_TICK: &[IdOrClass] =
797
    &[Class(AzString::from_const_str("__azul-native-statusbar-zoom-tick"))];
798
static CLS_ZOOM_LABEL: &[IdOrClass] =
799
    &[Class(AzString::from_const_str("__azul-native-statusbar-zoom-label"))];
800

            
801
// -- Constructors / builders --
802

            
803
impl StatusBar {
804
    /// Creates a status bar with the given left segments, no view switcher
805
    /// and no zoom cluster, in the the Office-2013-era look style.
806
    #[must_use]
807
11
    pub fn new(segments: StatusBarSegmentVec) -> Self {
808
11
        Self {
809
11
            segments,
810
11
            views: None.into(),
811
11
            zoom: None.into(),
812
11
            style: StatusBarStyle::office_2013(),
813
11
        }
814
11
    }
815

            
816
    /// Sets the view-switcher cluster.
817
3
    pub fn set_views(&mut self, views: StatusBarViewSwitcher) {
818
3
        self.views = Some(views).into();
819
3
    }
820

            
821
    /// Builder method: sets the view-switcher cluster and returns `self`.
822
    #[must_use]
823
3
    pub fn with_views(mut self, views: StatusBarViewSwitcher) -> Self {
824
3
        self.set_views(views);
825
3
        self
826
3
    }
827

            
828
    /// Sets the zoom cluster.
829
3
    pub fn set_zoom(&mut self, zoom: StatusBarZoom) {
830
3
        self.zoom = Some(zoom).into();
831
3
    }
832

            
833
    /// Builder method: sets the zoom cluster and returns `self`.
834
    #[must_use]
835
3
    pub fn with_zoom(mut self, zoom: StatusBarZoom) -> Self {
836
3
        self.set_zoom(zoom);
837
3
        self
838
3
    }
839

            
840
    /// Builder method: replaces the style bundle.
841
    #[must_use]
842
    pub fn with_style(mut self, style: StatusBarStyle) -> Self {
843
        self.style = style;
844
        self
845
    }
846

            
847
    /// Renders the status bar.
848
    #[must_use]
849
8
    pub fn dom(self) -> Dom {
850
8
        let Self { segments, views, zoom, style } = self;
851
8
        let mut children: Vec<Dom> = Vec::with_capacity(segments.len() + 3);
852

            
853
8
        for seg in segments.into_library_owned_vec() {
854
7
            children.push(segment_dom(seg, &style));
855
7
        }
856

            
857
8
        children.push(
858
8
            Dom::create_div()
859
8
                .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_FILLER))
860
8
                .with_css_props(style.filler_style.clone()),
861
        );
862

            
863
8
        if let Some(switcher) = views.into_option() {
864
3
            children.push(views_dom(switcher, &style));
865
5
        }
866

            
867
8
        if let Some(zoom) = zoom.into_option() {
868
3
            children.push(zoom_dom(zoom, &style));
869
5
        }
870

            
871
8
        Dom::create_div()
872
8
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_STATUSBAR))
873
8
            .with_css_props(style.bar_style)
874
8
            .with_children(DomVec::from_vec(children))
875
8
    }
876
}
877

            
878
impl Default for StatusBar {
879
    fn default() -> Self {
880
        Self::new(StatusBarSegmentVec::from_vec(Vec::new()))
881
    }
882
}
883

            
884
impl From<StatusBar> for Dom {
885
    fn from(s: StatusBar) -> Self {
886
        s.dom()
887
    }
888
}
889

            
890
// -- DOM builders --
891

            
892
/// Expands widget config to the existing [`Button`] widget with the
893
/// status-bar part styles injected (the ribbon's composition rule).
894
15
fn styled_button(
895
15
    icon: AzString,
896
15
    container_style: CssPropertyWithConditionsVec,
897
15
    icon_style: CssPropertyWithConditionsVec,
898
15
    on_click: OptionButtonOnClick,
899
15
) -> Dom {
900
15
    let mut b = Button::create(AzString::from_const_str(""));
901
15
    b.icon = icon;
902
15
    b.container_style = container_style;
903
15
    b.icon_style = icon_style;
904
15
    b.on_click = on_click;
905
15
    b.dom()
906
15
}
907

            
908
6
fn merged_style(
909
6
    base: &CssPropertyWithConditionsVec,
910
6
    extra: &CssPropertyWithConditionsVec,
911
6
) -> CssPropertyWithConditionsVec {
912
6
    if extra.as_ref().is_empty() {
913
        return base.clone();
914
6
    }
915
6
    let mut v: Vec<Cond> = base.as_ref().to_vec();
916
6
    v.extend_from_slice(extra.as_ref());
917
6
    CssPropertyWithConditionsVec::from_vec(v)
918
6
}
919

            
920
7
fn segment_dom(seg: StatusBarSegment, style: &StatusBarStyle) -> Dom {
921
7
    let StatusBarSegment { icon, label, on_click } = seg;
922
7
    if !icon.as_str().is_empty() || on_click.is_some() {
923
        // Icon and/or clickable: expand to a Button (flat chassis).
924
        let mut b = Button::create(label);
925
        b.icon = icon;
926
        b.container_style = style.segment_style.clone();
927
        b.icon_style = style.segment_icon_style.clone();
928
        b.label_style = style.segment_label_style.clone();
929
        b.on_click = on_click;
930
        return b
931
            .dom()
932
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_SEGMENT));
933
7
    }
934
    // Inert text segment.
935
7
    Dom::create_div()
936
7
        .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_SEGMENT))
937
7
        .with_css_props(style.segment_style.clone())
938
7
        .with_children(DomVec::from_vec(vec![
939
            // `<p>` so the inert segment has the same `div > p > text` shape as
940
            // the clickable one (Button puts `label_style` on a `<p>` too).
941
7
            Dom::create_p_with_text(label).with_css_props(style.segment_label_style.clone()),
942
        ]))
943
7
}
944

            
945
3
fn views_dom(switcher: StatusBarViewSwitcher, style: &StatusBarStyle) -> Dom {
946
3
    let StatusBarViewSwitcher { views, active_view, on_select } = switcher;
947
3
    let mut children: Vec<Dom> = Vec::with_capacity(views.len());
948
9
    for (idx, view) in views.into_library_owned_vec().into_iter().enumerate() {
949
9
        let container = if idx == active_view {
950
3
            merged_style(&style.view_button_style, &style.view_button_active_style)
951
        } else {
952
6
            style.view_button_style.clone()
953
        };
954
9
        let on_click: OptionButtonOnClick = match on_select.as_ref() {
955
3
            Some(cb) => Some(super::button::ButtonOnClick {
956
3
                refany: RefAny::new(ViewClickData { view_idx: idx, on_select: cb.clone() }),
957
3
                callback: super::button::ButtonOnClickCallback {
958
3
                    cb: on_status_bar_view_click,
959
3
                    ctx: azul_core::refany::OptionRefAny::None,
960
3
                },
961
3
            })
962
3
            .into(),
963
6
            None => None.into(),
964
        };
965
9
        children.push(styled_button(
966
9
            view.icon,
967
9
            container,
968
9
            style.view_icon_style.clone(),
969
9
            on_click,
970
        ));
971
    }
972
3
    Dom::create_div()
973
3
        .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_VIEWS))
974
3
        .with_css_props(style.views_style.clone())
975
3
        .with_children(DomVec::from_vec(children))
976
3
}
977

            
978
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] // bounded layout numeric cast
979
3
fn zoom_dom(zoom: StatusBarZoom, style: &StatusBarStyle) -> Dom {
980
3
    let StatusBarZoom { percent, min, max, on_zoom_out, on_zoom_in, on_slider_change, show_label } =
981
3
        zoom;
982

            
983
3
    let mut children: Vec<Dom> = Vec::with_capacity(4);
984

            
985
3
    children.push(styled_button(
986
3
        AzString::from_const_str("remove"),
987
3
        style.zoom_button_style.clone(),
988
3
        style.zoom_icon_style.clone(),
989
3
        on_zoom_out,
990
    ));
991

            
992
    // Rail + tick + embedded Slider, layered inside the positioning host.
993
3
    let fraction = if max > min {
994
3
        ((percent - min) / (max - min)).clamp(0.0, 1.0)
995
    } else {
996
        0.0
997
    };
998
3
    let margin = (fraction * (ZOOM_TRACK_W - ZOOM_THUMB_W) as f32).round() as isize;
999
3
    let thumb_style = merged_style(
3
        &style.slider_thumb_style,
3
        &CssPropertyWithConditionsVec::from_vec(vec![Cond::simple(P::const_margin_left(
3
            LayoutMarginLeft::const_px(margin),
3
        ))]),
    );
3
    let mut slider = Slider::create(percent, min, max);
3
    slider.track_style = style.slider_track_style.clone();
3
    slider.thumb_style = thumb_style;
3
    slider.slider_state.on_value_change = on_slider_change;
3
    children.push(
3
        Dom::create_div()
3
            .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_ZOOM_TRACK))
3
            .with_css_props(style.zoom_track_host_style.clone())
3
            .with_children(DomVec::from_vec(vec![
3
                Dom::create_div()
3
                    .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_ZOOM_RAIL))
3
                    .with_css_props(style.zoom_rail_style.clone()),
3
                Dom::create_div()
3
                    .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_ZOOM_TICK))
3
                    .with_css_props(style.zoom_tick_style.clone()),
3
                slider.dom(),
            ])),
    );
3
    children.push(styled_button(
3
        AzString::from_const_str("add"),
3
        style.zoom_button_style.clone(),
3
        style.zoom_icon_style.clone(),
3
        on_zoom_in,
    ));
3
    if show_label {
2
        let label = format!("{}%", percent.round() as i64);
2
        children.push(
2
            Dom::create_div()
2
                .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_ZOOM_LABEL))
2
                .with_css_props(style.zoom_label_style.clone())
2
                .with_children(DomVec::from_vec(vec![Dom::create_p_with_text(AzString::from(
2
                    label,
2
                ))])),
2
        );
2
    }
3
    Dom::create_div()
3
        .with_ids_and_classes(IdOrClassVec::from_const_slice(CLS_ZOOM))
3
        .with_css_props(style.zoom_style.clone())
3
        .with_children(DomVec::from_vec(children))
3
}
// -- View-click plumbing --
/// Payload of one view-switcher button: the view index plus the user's
/// view-select callback.
struct ViewClickData {
    view_idx: usize,
    on_select: StatusBarOnViewSelect,
}
extern "C" fn on_status_bar_view_click(mut data: RefAny, info: CallbackInfo) -> Update {
    let Some(payload) = data.downcast_ref::<ViewClickData>() else {
        return Update::DoNothing;
    };
    let idx = payload.view_idx;
    let cb = payload.on_select.callback.cb;
    let refany = payload.on_select.refany.clone();
    drop(payload);
    (cb)(refany, info, idx)
}
#[cfg(test)]
mod tests {
    use super::*;
9
    fn seg(label: &str) -> StatusBarSegment {
9
        StatusBarSegment::new(AzString::from(label))
9
    }
9
    fn segs(count: usize) -> StatusBarSegmentVec {
9
        StatusBarSegmentVec::from_vec((0..count).map(|i| seg(&format!("s{i}"))).collect())
9
    }
    // ------------------------------------------------------------------
    // Constructors and invariants
    // ------------------------------------------------------------------
    #[test]
1
    fn status_bar_new_defaults_to_office_2013_with_no_clusters() {
4
        for count in [0usize, 1, 3] {
3
            let s = StatusBar::new(segs(count));
3
            assert_eq!(s.segments.len(), count);
3
            assert!(s.views.is_none());
3
            assert!(s.zoom.is_none());
3
            assert_eq!(s.style, StatusBarStyle::office_2013());
        }
1
    }
    #[test]
1
    fn status_bar_style_default_is_office_2013() {
1
        assert_eq!(StatusBarStyle::default(), StatusBarStyle::office_2013());
1
    }
    #[test]
1
    fn zoom_office_2013_centers_the_default_percent() {
1
        let z = StatusBarZoom::office_2013();
        // 100% must rest exactly on the center tick of the [10, 190] window.
1
        let fraction = (z.percent - z.min) / (z.max - z.min);
1
        assert!((fraction - 0.5).abs() < 1e-6);
1
        assert!(z.show_label);
1
    }
    #[test]
1
    fn view_switcher_office_2013_has_three_views_with_print_layout_active() {
1
        let v = StatusBarViewSwitcher::office_2013();
1
        assert_eq!(v.views.len(), 3);
1
        assert_eq!(v.active_view, 1);
1
        assert!(v.on_select.is_none());
1
    }
    // ------------------------------------------------------------------
    // DOM shape
    // ------------------------------------------------------------------
    #[test]
1
    fn dom_renders_segments_filler_views_and_zoom_in_order() {
1
        let bar = StatusBar::new(segs(2))
1
            .with_views(StatusBarViewSwitcher::office_2013())
1
            .with_zoom(StatusBarZoom::office_2013());
1
        let dom = bar.dom();
        // 2 segments + filler + views + zoom
1
        assert_eq!(dom.children.as_ref().len(), 5);
1
    }
    #[test]
1
    fn dom_without_clusters_renders_segments_and_filler_only() {
1
        let dom = StatusBar::new(segs(3)).dom();
1
        assert_eq!(dom.children.as_ref().len(), 4);
1
    }
    #[test]
1
    fn zoom_cluster_without_label_has_three_children() {
1
        let mut zoom = StatusBarZoom::office_2013();
1
        zoom.show_label = false;
1
        let dom = StatusBar::new(segs(0)).with_zoom(zoom).dom();
        // filler + zoom
1
        assert_eq!(dom.children.as_ref().len(), 2);
1
        let zoom_dom = &dom.children.as_ref()[1];
        // − button, track host, + button (no label)
1
        assert_eq!(zoom_dom.children.as_ref().len(), 3);
1
    }
    #[test]
1
    fn zoom_track_host_layers_rail_tick_and_slider() {
1
        let dom = StatusBar::new(segs(0)).with_zoom(StatusBarZoom::office_2013()).dom();
1
        let zoom_dom = &dom.children.as_ref()[1];
1
        let track_host = &zoom_dom.children.as_ref()[1];
1
        assert_eq!(track_host.children.as_ref().len(), 3);
1
    }
    #[test]
1
    fn view_buttons_get_click_callbacks_only_with_a_select_handler() {
        extern "C" fn on_select(_: RefAny, _: CallbackInfo, _: usize) -> Update {
            Update::DoNothing
        }
        // Without a handler: no callbacks on the buttons.
1
        let dom = StatusBar::new(segs(0)).with_views(StatusBarViewSwitcher::office_2013()).dom();
1
        let views = &dom.children.as_ref()[1];
3
        for btn in views.children.as_ref() {
3
            assert!(btn.root.callbacks.as_ref().is_empty());
        }
        // With a handler: every button carries one.
1
        let mut switcher = StatusBarViewSwitcher::office_2013();
1
        switcher.set_on_select(RefAny::new(()), on_select as StatusBarOnViewSelectCallbackType);
1
        let dom = StatusBar::new(segs(0)).with_views(switcher).dom();
1
        let views = &dom.children.as_ref()[1];
3
        for btn in views.children.as_ref() {
3
            assert_eq!(btn.root.callbacks.as_ref().len(), 1);
        }
1
    }
}