1
//! Microsoft Office-style ribbon widget.
2
//!
3
//! A [`Ribbon`] organizes controls into a tabbed toolbar where each tab
4
//! contains one or more [`RibbonSection`]s, each with a title and arbitrary
5
//! content.  Unlike the simpler [`super::tabs`] widget, each tab is further
6
//! subdivided into titled, visually separated sections — matching the ribbon
7
//! pattern found in Office applications.
8

            
9
use azul_core::{
10
    callbacks::{CoreCallback, CoreCallbackData, Update},
11
    dom::{Dom, DomVec, EventFilter, HoverEventFilter, IdOrClass, IdOrClass::Class, IdOrClassVec},
12
    refany::RefAny,
13
};
14
use azul_css::{
15
    dynamic_selector::{CssPropertyWithConditions as Cond, CssPropertyWithConditionsVec},
16
    props::{
17
        basic::{color::ColorU, font::{StyleFontFamily, StyleFontFamilyVec}, *},
18
        layout::*,
19
        property::CssProperty as P,
20
        style::*,
21
    },
22
    *,
23
};
24

            
25
use azul_css::{impl_option, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_partialeq, impl_vec_mut};
26

            
27
use crate::callbacks::{Callback, CallbackInfo};
28

            
29
// -- Callback --
30

            
31
/// Callback signature invoked when a ribbon tab is clicked.
32
pub type RibbonOnTabClickCallbackType = extern "C" fn(RefAny, CallbackInfo, usize) -> Update;
33
impl_widget_callback!(
34
    RibbonOnTabClick, OptionRibbonOnTabClick,
35
    RibbonOnTabClickCallback, RibbonOnTabClickCallbackType
36
);
37

            
38
azul_core::impl_managed_callback! {
39
    wrapper:        RibbonOnTabClickCallback,
40
    info_ty:        CallbackInfo,
41
    return_ty:      Update,
42
    default_ret:    Update::DoNothing,
43
    invoker_static: RIBBON_ON_TAB_CLICK_INVOKER,
44
    invoker_ty:     AzRibbonOnTabClickCallbackInvoker,
45
    thunk_fn:       az_ribbon_on_tab_click_callback_thunk,
46
    setter_fn:      AzApp_setRibbonOnTabClickCallbackInvoker,
47
    from_handle_fn: AzRibbonOnTabClickCallback_createFromHostHandle,
48
    extra_args:     [ tab_index: usize ],
49
}
50

            
51
// -- Font --
52

            
53
const SYSTEM_UI_STR: AzString = AzString::from_const_str("system:ui");
54
const SYSTEM_UI_FAMILIES: &[StyleFontFamily] = &[StyleFontFamily::System(SYSTEM_UI_STR)];
55
const SYSTEM_UI_FAMILY: StyleFontFamilyVec =
56
    StyleFontFamilyVec::from_const_slice(SYSTEM_UI_FAMILIES);
57

            
58
// -- Colors --
59

            
60
const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
61
const LIGHT_GRAY: ColorU = ColorU { r: 240, g: 240, b: 240, a: 255 };
62
const BORDER_GRAY: ColorU = ColorU { r: 200, g: 200, b: 200, a: 255 };
63
const TEXT_GRAY: ColorU = ColorU { r: 100, g: 100, b: 100, a: 255 };
64
const ACTIVE_BLUE: ColorU = ColorU { r: 0, g: 114, b: 198, a: 255 };
65
const BG_WHITE: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(WHITE)];
66
const BG_LIGHT_GRAY: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(LIGHT_GRAY)];
67

            
68
static RIBBON_CONTAINER_STYLE: &[Cond] = &[
69
    Cond::simple(P::const_display(LayoutDisplay::Flex)),
70
    Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
71
    Cond::simple(P::const_font_family(SYSTEM_UI_FAMILY)),
72
    Cond::simple(P::const_font_size(StyleFontSize::const_px(12))),
73
];
74

            
75
static TAB_BAR_STYLE: &[Cond] = &[
76
    Cond::simple(P::const_display(LayoutDisplay::Flex)),
77
    Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
78
    Cond::simple(P::const_background_content(StyleBackgroundContentVec::from_const_slice(BG_LIGHT_GRAY))),
79
    Cond::simple(P::const_border_bottom_width(LayoutBorderBottomWidth::const_px(1))),
80
    Cond::simple(P::const_border_bottom_style(StyleBorderBottomStyle { inner: BorderStyle::Solid })),
81
    Cond::simple(P::const_border_bottom_color(StyleBorderBottomColor { inner: BORDER_GRAY })),
82
];
83

            
84
static TAB_INACTIVE_STYLE: &[Cond] = &[
85
    Cond::simple(P::const_padding_left(LayoutPaddingLeft::const_px(12))),
86
    Cond::simple(P::const_padding_right(LayoutPaddingRight::const_px(12))),
87
    Cond::simple(P::const_padding_top(LayoutPaddingTop::const_px(6))),
88
    Cond::simple(P::const_padding_bottom(LayoutPaddingBottom::const_px(6))),
89
    Cond::simple(P::const_cursor(StyleCursor::Pointer)),
90
    Cond::simple(P::const_text_color(StyleTextColor { inner: TEXT_GRAY })),
91
];
92

            
93
static TAB_ACTIVE_STYLE: &[Cond] = &[
94
    Cond::simple(P::const_padding_left(LayoutPaddingLeft::const_px(12))),
95
    Cond::simple(P::const_padding_right(LayoutPaddingRight::const_px(12))),
96
    Cond::simple(P::const_padding_top(LayoutPaddingTop::const_px(6))),
97
    Cond::simple(P::const_padding_bottom(LayoutPaddingBottom::const_px(6))),
98
    Cond::simple(P::const_cursor(StyleCursor::Pointer)),
99
    Cond::simple(P::const_background_content(StyleBackgroundContentVec::from_const_slice(BG_WHITE))),
100
    Cond::simple(P::const_border_bottom_width(LayoutBorderBottomWidth::const_px(2))),
101
    Cond::simple(P::const_border_bottom_style(StyleBorderBottomStyle { inner: BorderStyle::Solid })),
102
    Cond::simple(P::const_border_bottom_color(StyleBorderBottomColor { inner: ACTIVE_BLUE })),
103
];
104

            
105
static SECTIONS_CONTAINER_STYLE: &[Cond] = &[
106
    Cond::simple(P::const_display(LayoutDisplay::Flex)),
107
    Cond::simple(P::const_flex_direction(LayoutFlexDirection::Row)),
108
    Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
109
    Cond::simple(P::const_background_content(StyleBackgroundContentVec::from_const_slice(BG_WHITE))),
110
    Cond::simple(P::const_padding_top(LayoutPaddingTop::const_px(4))),
111
    Cond::simple(P::const_padding_bottom(LayoutPaddingBottom::const_px(4))),
112
    Cond::simple(P::const_padding_left(LayoutPaddingLeft::const_px(4))),
113
    Cond::simple(P::const_padding_right(LayoutPaddingRight::const_px(4))),
114
    Cond::simple(P::const_border_bottom_width(LayoutBorderBottomWidth::const_px(1))),
115
    Cond::simple(P::const_border_bottom_style(StyleBorderBottomStyle { inner: BorderStyle::Solid })),
116
    Cond::simple(P::const_border_bottom_color(StyleBorderBottomColor { inner: BORDER_GRAY })),
117
];
118

            
119
static SECTION_STYLE: &[Cond] = &[
120
    Cond::simple(P::const_display(LayoutDisplay::Flex)),
121
    Cond::simple(P::const_flex_direction(LayoutFlexDirection::Column)),
122
    Cond::simple(P::const_padding_left(LayoutPaddingLeft::const_px(6))),
123
    Cond::simple(P::const_padding_right(LayoutPaddingRight::const_px(6))),
124
    Cond::simple(P::const_border_right_width(LayoutBorderRightWidth::const_px(1))),
125
    Cond::simple(P::const_border_right_style(StyleBorderRightStyle { inner: BorderStyle::Solid })),
126
    Cond::simple(P::const_border_right_color(StyleBorderRightColor { inner: BORDER_GRAY })),
127
];
128

            
129
static SECTION_CONTENT_STYLE: &[Cond] = &[
130
    Cond::simple(P::const_flex_grow(LayoutFlexGrow::const_new(1))),
131
];
132

            
133
static SECTION_TITLE_STYLE: &[Cond] = &[
134
    Cond::simple(P::const_font_size(StyleFontSize::const_px(11))),
135
    Cond::simple(P::const_text_color(StyleTextColor { inner: TEXT_GRAY })),
136
    Cond::simple(P::const_text_align(StyleTextAlign::Center)),
137
    Cond::simple(P::const_padding_top(LayoutPaddingTop::const_px(2))),
138
];
139

            
140
/// Top-level ribbon widget containing multiple tabs.
141
#[derive(Debug, Clone)]
142
#[repr(C)]
143
pub struct Ribbon {
144
    /// Tabs displayed in the ribbon tab bar.
145
    pub tabs: RibbonTabVec,
146
    /// Index of the currently active tab.
147
    pub active_tab: usize,
148
    /// Optional callback fired when a tab is clicked.
149
    pub on_tab_click: OptionRibbonOnTabClick,
150
}
151

            
152
/// A single tab within a [`Ribbon`], containing a label and sections.
153
#[derive(Debug, Clone)]
154
#[repr(C)]
155
pub struct RibbonTab {
156
    /// Display label shown in the tab bar.
157
    pub label: AzString,
158
    /// Sections rendered when this tab is active.
159
    pub sections: RibbonSectionVec,
160
}
161

            
162
/// A titled section within a [`RibbonTab`], holding arbitrary content.
163
#[derive(Debug, Clone)]
164
#[repr(C)]
165
pub struct RibbonSection {
166
    /// Title displayed below the section content.
167
    pub title: AzString,
168
    /// Content DOM rendered inside this section.
169
    pub content: Dom,
170
}
171

            
172
impl_option!(RibbonSection, OptionRibbonSection, copy = false, [Debug, Clone]);
173
impl_vec!(RibbonSection, RibbonSectionVec, RibbonSectionVecDestructor, RibbonSectionVecDestructorType, RibbonSectionVecSlice, OptionRibbonSection);
174
impl_vec_clone!(RibbonSection, RibbonSectionVec, RibbonSectionVecDestructor);
175
impl_vec_debug!(RibbonSection, RibbonSectionVec);
176
impl_vec_mut!(RibbonSection, RibbonSectionVec);
177

            
178
impl_option!(RibbonTab, OptionRibbonTab, copy = false, [Debug, Clone]);
179
impl_vec!(RibbonTab, RibbonTabVec, RibbonTabVecDestructor, RibbonTabVecDestructorType, RibbonTabVecSlice, OptionRibbonTab);
180
impl_vec_clone!(RibbonTab, RibbonTabVec, RibbonTabVecDestructor);
181
impl_vec_debug!(RibbonTab, RibbonTabVec);
182
impl_vec_mut!(RibbonTab, RibbonTabVec);
183

            
184
impl RibbonTab {
185
    /// Creates a new tab with the given label and no sections.
186
    pub fn new(label: AzString) -> Self {
187
        Self { label, sections: RibbonSectionVec::from_const_slice(&[]) }
188
    }
189

            
190
    /// Appends a section to this tab.
191
    pub fn add_section(&mut self, section: RibbonSection) {
192
        self.sections.push(section);
193
    }
194

            
195
    /// Builder method: appends a section and returns `self`.
196
    pub fn with_section(mut self, section: RibbonSection) -> Self {
197
        self.add_section(section);
198
        self
199
    }
200
}
201

            
202
impl RibbonSection {
203
    /// Creates a new section with the given title and content DOM.
204
    pub fn new(title: AzString, content: Dom) -> Self {
205
        Self { title, content }
206
    }
207
}
208

            
209
impl Ribbon {
210
    /// Creates a new ribbon with the given tabs, defaulting to the first tab active.
211
    pub fn new(tabs: RibbonTabVec) -> Self {
212
        Self { tabs, active_tab: 0, on_tab_click: None.into() }
213
    }
214

            
215
    /// Sets the active tab by index, clamping to the last valid tab.
216
    pub fn set_active_tab(&mut self, index: usize) {
217
        let max = self.tabs.len().saturating_sub(1);
218
        self.active_tab = if index > max { max } else { index };
219
    }
220

            
221
    /// Registers a callback invoked when a tab is clicked.
222
    pub fn set_on_tab_click<C: Into<RibbonOnTabClickCallback>>(&mut self, data: RefAny, cb: C) {
223
        self.on_tab_click = Some(RibbonOnTabClick {
224
            callback: cb.into(), refany: data,
225
        }).into();
226
    }
227

            
228
    /// Builder method: registers a tab-click callback and returns `self`.
229
    pub fn with_on_tab_click<C: Into<RibbonOnTabClickCallback>>(mut self, data: RefAny, cb: C) -> Self {
230
        self.set_on_tab_click(data, cb);
231
        self
232
    }
233

            
234
    /// Builds the ribbon DOM, rendering the tab bar and the active tab's sections.
235
    pub fn dom(self) -> Dom {
236
        let active_tab = self.active_tab;
237
        let has_callback = self.on_tab_click.is_some();
238

            
239
        let tab_items: Vec<Dom> = self.tabs.as_slice().iter().enumerate().map(|(idx, tab)| {
240
            let style = if idx == active_tab { TAB_ACTIVE_STYLE } else { TAB_INACTIVE_STYLE };
241
            let mut d = Dom::create_text(tab.label.clone())
242
                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(style));
243
            if has_callback {
244
                d = d.with_callbacks(vec![CoreCallbackData {
245
                    event: EventFilter::Hover(HoverEventFilter::MouseUp),
246
                    callback: CoreCallback {
247
                        cb: on_ribbon_tab_click as usize,
248
                        ctx: azul_core::refany::OptionRefAny::None,
249
                    },
250
                    refany: RefAny::new(TabClickData {
251
                        tab_idx: idx, on_tab_click: self.on_tab_click.clone(),
252
                    }),
253
                }].into());
254
            }
255
            d
256
        }).collect();
257

            
258
        let tab_bar = Dom::create_div()
259
            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(TAB_BAR_STYLE))
260
            .with_children(DomVec::from_vec(tab_items));
261

            
262
        let sections_dom = if let Some(active) = self.tabs.into_library_owned_vec().into_iter().nth(active_tab) {
263
            let items: Vec<Dom> = active.sections.into_library_owned_vec().into_iter().map(|s| {
264
                let content = Dom::create_div()
265
                    .with_css_props(CssPropertyWithConditionsVec::from_const_slice(SECTION_CONTENT_STYLE))
266
                    .with_children(DomVec::from_vec(vec![s.content]));
267
                let title = Dom::create_text(s.title)
268
                    .with_css_props(CssPropertyWithConditionsVec::from_const_slice(SECTION_TITLE_STYLE));
269
                Dom::create_div()
270
                    .with_css_props(CssPropertyWithConditionsVec::from_const_slice(SECTION_STYLE))
271
                    .with_children(DomVec::from_vec(vec![content, title]))
272
            }).collect();
273
            Dom::create_div()
274
                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(SECTIONS_CONTAINER_STYLE))
275
                .with_children(DomVec::from_vec(items))
276
        } else {
277
            Dom::create_div()
278
                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(SECTIONS_CONTAINER_STYLE))
279
        };
280

            
281
        Dom::create_div()
282
            .with_css_props(CssPropertyWithConditionsVec::from_const_slice(RIBBON_CONTAINER_STYLE))
283
            .with_ids_and_classes({
284
                const CLS: &[IdOrClass] = &[Class(AzString::from_const_str("__azul-native-ribbon"))];
285
                IdOrClassVec::from_const_slice(CLS)
286
            })
287
            .with_children(DomVec::from_vec(vec![tab_bar, sections_dom]))
288
    }
289
}
290

            
291
struct TabClickData {
292
    tab_idx: usize,
293
    on_tab_click: OptionRibbonOnTabClick,
294
}
295

            
296
extern "C" fn on_ribbon_tab_click(mut refany: RefAny, info: CallbackInfo) -> Update {
297
    let mut data = match refany.downcast_mut::<TabClickData>() {
298
        Some(d) => d,
299
        None => return Update::DoNothing,
300
    };
301
    let idx = data.tab_idx;
302
    match data.on_tab_click.as_mut() {
303
        Some(RibbonOnTabClick { refany, callback }) => {
304
            (callback.cb)(refany.clone(), info.clone(), idx)
305
        }
306
        None => Update::DoNothing,
307
    }
308
}
309

            
310
impl From<Ribbon> for Dom {
311
    fn from(r: Ribbon) -> Dom { r.dom() }
312
}