Skip to main content

azul_layout/widgets/
titlebar.rs

1//! Titlebar widget for custom window chrome (CSD and title-only modes).
2//!
3//! Key type: [`Titlebar`]
4
5use azul_core::{
6    dom::{Dom, DomVec, IdOrClass, IdOrClass::Class, IdOrClass::Id, IdOrClassVec},
7    refany::RefAny,
8};
9use azul_css::{
10    dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
11    props::{
12        basic::{
13            color::ColorU,
14            font::{StyleFontFamily, StyleFontFamilyVec},
15            *,
16        },
17        layout::*,
18        property::{CssProperty, *},
19        style::*,
20    },
21    system::{SystemFontType, SystemStyle, TitlebarButtonSide, TitlebarButtons, TitlebarMetrics},
22    *,
23};
24
25// ── Compile-time defaults (used when no SystemStyle is available) ─────────
26
27// Verified: macOS 11 Big Sur – macOS 15 Sequoia (2020–2025)
28#[cfg(target_os = "macos")]
29const DEFAULT_TITLEBAR_HEIGHT: f32 = 28.0;
30#[cfg(target_os = "windows")]
31const DEFAULT_TITLEBAR_HEIGHT: f32 = 32.0;
32#[cfg(target_os = "linux")]
33const DEFAULT_TITLEBAR_HEIGHT: f32 = 30.0;
34#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
35const DEFAULT_TITLEBAR_HEIGHT: f32 = 32.0;
36
37#[cfg(target_os = "macos")]
38const DEFAULT_TITLE_FONT_SIZE: f32 = 13.0;
39#[cfg(target_os = "windows")]
40const DEFAULT_TITLE_FONT_SIZE: f32 = 12.0;
41#[cfg(target_os = "linux")]
42const DEFAULT_TITLE_FONT_SIZE: f32 = 13.0;
43#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
44const DEFAULT_TITLE_FONT_SIZE: f32 = 13.0;
45
46// Verified: macOS 11–15 traffic-light geometry = 78px including gaps
47#[cfg(target_os = "macos")]
48const DEFAULT_BUTTON_AREA_WIDTH: f32 = 78.0;
49// Windows 10/11: 3 buttons × 46px = 138px
50#[cfg(target_os = "windows")]
51const DEFAULT_BUTTON_AREA_WIDTH: f32 = 138.0;
52#[cfg(target_os = "linux")]
53const DEFAULT_BUTTON_AREA_WIDTH: f32 = 100.0;
54#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
55const DEFAULT_BUTTON_AREA_WIDTH: f32 = 100.0;
56
57// macOS: traffic lights on the left.  All others: right.
58#[cfg(target_os = "macos")]
59const DEFAULT_BUTTON_SIDE_LEFT: bool = true;
60#[cfg(not(target_os = "macos"))]
61const DEFAULT_BUTTON_SIDE_LEFT: bool = false;
62
63// Default title text color for light / dark fallback
64const DEFAULT_TITLE_COLOR_LIGHT: ColorU = ColorU { r: 76, g: 76, b: 76, a: 255 };  // #4c4c4c
65const DEFAULT_TITLE_COLOR_DARK: ColorU = ColorU { r: 229, g: 229, b: 229, a: 255 }; // #e5e5e5
66
67// ── Titlebar ─────────────────────────────────────────────────────────────
68
69/// A titlebar widget with optional close / minimize / maximize
70/// buttons, drag-to-move, and double-click-to-maximize.
71///
72/// # Two modes
73///
74/// 1. **Title-only** ([`Titlebar::dom`], the default for
75///    `WindowDecorations::NoTitleAutoInject`):
76///    The OS still draws the native window-control buttons (traffic lights on
77///    macOS, caption buttons on Windows).  The titlebar reserves
78///    `padding_left` / `padding_right` so the title text doesn't overlap them.
79///
80/// 2. **Full CSD** ([`Titlebar::dom_with_buttons`], used when
81///    `WindowDecorations::None` + `has_decorations`):
82///    The titlebar renders its own close / minimize / maximize buttons as
83///    regular DOM nodes.  Each button carries a plain `MouseDown` callback
84///    that calls `CallbackInfo::modify_window_state()` — exactly the same
85///    mechanism used for window dragging.  No special event-system hooks.
86///
87/// Window-control buttons use `Dom::create_icon("close")` etc. so that
88/// icons are resolved through the icon provider system (Material Icons
89/// by default) and can be swapped out by registering a different icon pack.
90///
91/// # Button layout
92///
93/// `button_side` controls where the buttons appear:
94/// - `Left` — macOS traffic-light style (buttons before title)
95/// - `Right` — Windows / Linux style (title then buttons)
96///
97/// # Styling
98///
99/// The DOM uses CSS classes `.csd-titlebar`, `.csd-title`, `.csd-buttons`,
100/// `.csd-button`, `.csd-close`, `.csd-minimize`, `.csd-maximize`.
101/// These match the output of `SystemStyle::create_csd_stylesheet()`.
102#[derive(Debug, Clone, PartialEq, PartialOrd)]
103#[repr(C)]
104pub struct Titlebar {
105    /// The title text to display.
106    pub title: AzString,
107    /// Height of the titlebar in CSS pixels.
108    pub height: f32,
109    /// Font size for the title text in CSS pixels.
110    pub font_size: f32,
111    /// Extra padding on the **left** side (px).
112    pub padding_left: f32,
113    /// Extra padding on the **right** side (px).
114    pub padding_right: f32,
115    /// Title text color (resolved from SystemStyle.colors.text or platform default).
116    pub title_color: ColorU,
117}
118
119impl Titlebar {
120    /// Create a titlebar with compile-time platform defaults.
121    ///
122    /// Use [`Titlebar::from_system_style`] when you have a
123    /// `SystemStyle` available for pixel-perfect metrics.
124    #[inline]
125    pub fn new(title: AzString) -> Self {
126        // Equal padding on both sides keeps text-align:center at the window midpoint.
127        // The button-side half prevents overlap; the opposite half balances it.
128        let half = DEFAULT_BUTTON_AREA_WIDTH / 2.0;
129        let (padding_left, padding_right) = (half, half);
130        Self {
131            title,
132            height: DEFAULT_TITLEBAR_HEIGHT,
133            font_size: DEFAULT_TITLE_FONT_SIZE,
134            padding_left,
135            padding_right,
136            title_color: DEFAULT_TITLE_COLOR_LIGHT,
137        }
138    }
139
140    /// FFI-compatible alias for [`Titlebar::new`].
141    #[inline]
142    pub fn create(title: AzString) -> Self {
143        Self::new(title)
144    }
145
146    /// Create a titlebar with a custom height.
147    #[inline]
148    pub fn with_height(title: AzString, height: f32) -> Self {
149        let mut tb = Self::new(title);
150        tb.height = height;
151        tb
152    }
153
154    /// Set the titlebar height.
155    #[inline]
156    pub fn set_height(&mut self, height: f32) {
157        self.height = height;
158    }
159
160    /// Set the title text.
161    #[inline]
162    pub fn set_title(&mut self, title: AzString) {
163        self.title = title;
164    }
165
166    /// Swap this titlebar with a default instance, returning the old value.
167    #[inline]
168    pub fn swap_with_default(&mut self) -> Self {
169        let mut s = Titlebar::new(AzString::from_const_str(""));
170        core::mem::swap(&mut s, self);
171        s
172    }
173
174    /// Create from a live [`SystemStyle`] (for title-only mode, padding
175    /// reserves space for OS-drawn buttons).
176    pub fn from_system_style(title: AzString, system_style: &SystemStyle) -> Self {
177        let tm = &system_style.metrics.titlebar;
178        let height = tm.height.as_ref()
179            .map(|pv| pv.to_pixels_internal(0.0, 0.0, 0.0))
180            .unwrap_or(DEFAULT_TITLEBAR_HEIGHT);
181        let font_size = tm.title_font_size
182            .into_option()
183            .unwrap_or(DEFAULT_TITLE_FONT_SIZE);
184        let button_area = tm.button_area_width.as_ref()
185            .map(|pv| pv.to_pixels_internal(0.0, 0.0, 0.0))
186            .unwrap_or(DEFAULT_BUTTON_AREA_WIDTH);
187        let safe_left = tm.safe_area.left.as_ref()
188            .map(|pv| pv.to_pixels_internal(0.0, 0.0, 0.0))
189            .unwrap_or(0.0);
190        let safe_right = tm.safe_area.right.as_ref()
191            .map(|pv| pv.to_pixels_internal(0.0, 0.0, 0.0))
192            .unwrap_or(0.0);
193        // Apply padding_horizontal from TitlebarMetrics
194        let pad_h = tm.padding_horizontal.as_ref()
195            .map(|pv| pv.to_pixels_internal(0.0, 0.0, 0.0))
196            .unwrap_or(0.0);
197
198        // Equal padding on both sides so text-align:center stays at the window midpoint.
199        // button_area/2 on each side: the button-side half clears the traffic-lights/caption
200        // buttons, the opposite half balances the centering offset.
201        let half_btn = button_area / 2.0;
202        let (padding_left, padding_right) = (
203            half_btn + safe_left + pad_h,
204            half_btn + safe_right + pad_h,
205        );
206
207        // Resolve title color from system style, with dark/light fallback
208        let title_color = system_style.colors.text.into_option().unwrap_or(
209            match system_style.theme {
210                azul_css::system::Theme::Dark => DEFAULT_TITLE_COLOR_DARK,
211                azul_css::system::Theme::Light => DEFAULT_TITLE_COLOR_LIGHT,
212            }
213        );
214
215        Self { title, height, font_size, padding_left, padding_right, title_color }
216    }
217
218    /// Create from [`SystemStyle`] for **full CSD** mode (no padding — the
219    /// buttons are rendered as DOM children).
220    pub fn from_system_style_csd(title: AzString, system_style: &SystemStyle) -> Self {
221        let tm = &system_style.metrics.titlebar;
222        let height = tm.height.as_ref()
223            .map(|pv| pv.to_pixels_internal(0.0, 0.0, 0.0))
224            .unwrap_or(DEFAULT_TITLEBAR_HEIGHT);
225        let font_size = tm.title_font_size
226            .into_option()
227            .unwrap_or(DEFAULT_TITLE_FONT_SIZE);
228        let title_color = system_style.colors.text.into_option().unwrap_or(
229            match system_style.theme {
230                azul_css::system::Theme::Dark => DEFAULT_TITLE_COLOR_DARK,
231                azul_css::system::Theme::Light => DEFAULT_TITLE_COLOR_LIGHT,
232            }
233        );
234        Self { title, height, font_size, padding_left: 0.0, padding_right: 0.0, title_color }
235    }
236
237    /// Build inline CSS for the container div.
238    fn build_container_style(&self, show_buttons: bool) -> CssPropertyWithConditionsVec {
239        let mut props = Vec::with_capacity(8);
240        if show_buttons {
241            // CSD mode: flex layout to place buttons + title side by side
242            props.push(CssPropertyWithConditions::simple(
243                CssProperty::const_display(LayoutDisplay::Flex),
244            ));
245            props.push(CssPropertyWithConditions::simple(
246                CssProperty::const_flex_direction(LayoutFlexDirection::Row),
247            ));
248            props.push(CssPropertyWithConditions::simple(
249                CssProperty::const_align_items(LayoutAlignItems::Center),
250            ));
251        } else {
252            // Title-only mode: block layout — title fills width automatically.
253            // Avoids flex-grow complexity; text centers via text-align.
254            props.push(CssPropertyWithConditions::simple(
255                CssProperty::const_display(LayoutDisplay::Block),
256            ));
257        }
258        props.push(CssPropertyWithConditions::simple(
259            CssProperty::const_height(LayoutHeight::const_px(self.height as isize)),
260        ));
261        // Titlebar should show grab cursor and prevent text selection
262        props.push(CssPropertyWithConditions::simple(
263            CssProperty::const_cursor(StyleCursor::Grab),
264        ));
265        props.push(CssPropertyWithConditions::simple(
266            CssProperty::user_select(StyleUserSelect::None),
267        ));
268        if self.padding_left > 0.0 {
269            props.push(CssPropertyWithConditions::simple(
270                CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
271                    self.padding_left as isize,
272                )),
273            ));
274        }
275        if self.padding_right > 0.0 {
276            props.push(CssPropertyWithConditions::simple(
277                CssProperty::const_padding_right(LayoutPaddingRight::const_px(
278                    self.padding_right as isize,
279                )),
280            ));
281        }
282        CssPropertyWithConditionsVec::from_vec(props)
283    }
284
285    /// Build inline CSS for the title text node.
286    fn build_title_style(&self, show_buttons: bool) -> CssPropertyWithConditionsVec {
287        let font_family = StyleFontFamilyVec::from_vec(vec![
288            StyleFontFamily::SystemType(SystemFontType::TitleBold),
289        ]);
290        let mut props = Vec::with_capacity(10);
291        props.push(CssPropertyWithConditions::simple(
292            CssProperty::const_font_size(StyleFontSize::const_px(self.font_size as isize)),
293        ));
294        props.push(CssPropertyWithConditions::simple(
295            CssProperty::const_font_family(font_family),
296        ));
297        // Use resolved title color from SystemStyle (adapts to dark mode)
298        props.push(CssPropertyWithConditions::simple(
299            CssProperty::const_text_color(StyleTextColor { inner: self.title_color }),
300        ));
301        // In CSD mode (flex container), title must grow to fill remaining space
302        if show_buttons {
303            props.push(CssPropertyWithConditions::simple(
304                CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1)),
305            ));
306            props.push(CssPropertyWithConditions::simple(
307                CssProperty::const_min_width(LayoutMinWidth::const_px(0)),
308            ));
309        }
310        props.push(CssPropertyWithConditions::simple(
311            CssProperty::const_text_align(StyleTextAlign::Center),
312        ));
313        props.push(CssPropertyWithConditions::simple(
314            CssProperty::WhiteSpace(StyleWhiteSpaceValue::Exact(StyleWhiteSpace::Nowrap)),
315        ));
316        props.push(CssPropertyWithConditions::simple(
317            CssProperty::const_overflow_x(LayoutOverflow::Hidden),
318        ));
319        // Vertically center the text: pad from top by (height - font_size) / 2
320        let v_pad = ((self.height - self.font_size) / 2.0).max(0.0);
321        if v_pad > 0.0 {
322            props.push(CssPropertyWithConditions::simple(
323                CssProperty::const_padding_top(LayoutPaddingTop::const_px(v_pad as isize)),
324            ));
325        }
326        CssPropertyWithConditionsVec::from_vec(props)
327    }
328
329    /// Title-only DOM (for `NoTitleAutoInject`).
330    ///
331    /// The OS draws the native window-control buttons; this just renders
332    /// a centred title with drag support.
333    #[inline]
334    pub fn dom(self) -> Dom {
335        self.dom_inner(false, &TitlebarButtons::default(), TitlebarButtonSide::Right)
336    }
337
338    /// Full-CSD DOM with close / minimize / maximize buttons.
339    ///
340    /// Each button is a div with a `MouseDown` callback that calls
341    /// `modify_window_state()` — no special hooks needed.
342    pub fn dom_with_buttons(
343        self,
344        buttons: &TitlebarButtons,
345        button_side: TitlebarButtonSide,
346    ) -> Dom {
347        self.dom_inner(true, buttons, button_side)
348    }
349
350    /// Inner builder for both modes.
351    fn dom_inner(
352        self,
353        show_buttons: bool,
354        buttons: &TitlebarButtons,
355        button_side: TitlebarButtonSide,
356    ) -> Dom {
357        use azul_core::{
358            callbacks::{CoreCallback, CoreCallbackData},
359            dom::{EventFilter, HoverEventFilter},
360        };
361
362        #[derive(Debug, Clone, Copy)]
363        struct DragMarker;
364
365        // Build styles BEFORE moving self.title
366        let title_style = self.build_title_style(show_buttons);
367        let container_style = self.build_container_style(show_buttons);
368
369        // ── Title node with drag callbacks ──
370        let title_classes = IdOrClassVec::from_vec(vec![Class("csd-title".into())]);
371
372        let title_node = Dom::create_div()
373            .with_ids_and_classes(title_classes)
374            .with_css_props(title_style)
375            .with_child(Dom::create_text(self.title)) // moves self.title
376            .with_callbacks(vec![
377                CoreCallbackData {
378                    event: EventFilter::Hover(HoverEventFilter::DragStart),
379                    callback: CoreCallback {
380                        cb: self::callbacks::titlebar_drag_start as usize,
381                        ctx: azul_core::refany::OptionRefAny::None,
382                    },
383                    refany: RefAny::new(DragMarker),
384                },
385                CoreCallbackData {
386                    event: EventFilter::Hover(HoverEventFilter::Drag),
387                    callback: CoreCallback {
388                        cb: self::callbacks::titlebar_drag as usize,
389                        ctx: azul_core::refany::OptionRefAny::None,
390                    },
391                    refany: RefAny::new(DragMarker),
392                },
393                CoreCallbackData {
394                    event: EventFilter::Hover(HoverEventFilter::DoubleClick),
395                    callback: CoreCallback {
396                        cb: self::callbacks::titlebar_double_click as usize,
397                        ctx: azul_core::refany::OptionRefAny::None,
398                    },
399                    refany: RefAny::new(DragMarker),
400                },
401            ].into());
402
403        // ── Button container (CSD mode only) ──
404        let button_container = if show_buttons {
405            Some(build_button_container(buttons))
406        } else {
407            None
408        };
409
410        // ── Root ──
411        let container_classes = IdOrClassVec::from_vec(vec![
412            Class("csd-titlebar".into()),
413            Class("__azul-native-titlebar".into()),
414        ]);
415        let mut root = Dom::create_div()
416            .with_ids_and_classes(container_classes)
417            .with_css_props(container_style);
418
419        // Button side determines child order:
420        //   Left  (macOS):   [buttons] [title]
421        //   Right (Win/Lin): [title] [buttons]
422        match button_side {
423            TitlebarButtonSide::Left => {
424                if let Some(btn) = button_container { root = root.with_child(btn); }
425                root = root.with_child(title_node);
426            }
427            TitlebarButtonSide::Right => {
428                root = root.with_child(title_node);
429                if let Some(btn) = button_container { root = root.with_child(btn); }
430            }
431        }
432
433        root
434    }
435}
436
437/// Build the `.csd-buttons` container with close/min/max button DOM nodes.
438fn build_button_container(buttons: &TitlebarButtons) -> Dom {
439    use azul_core::{
440        callbacks::{CoreCallback, CoreCallbackData},
441        dom::{EventFilter, HoverEventFilter},
442    };
443
444    let mut children = Vec::new();
445
446    if buttons.has_minimize {
447        let classes = IdOrClassVec::from_vec(vec![
448            Id("csd-button-minimize".into()),
449            Class("csd-button".into()),
450            Class("csd-minimize".into()),
451        ]);
452        children.push(Dom::create_div()
453            .with_ids_and_classes(classes)
454            .with_child(Dom::create_icon("minimize"))
455            .with_callbacks(vec![CoreCallbackData {
456                event: EventFilter::Hover(HoverEventFilter::MouseDown),
457                callback: CoreCallback {
458                    cb: self::callbacks::csd_minimize as usize,
459                    ctx: azul_core::refany::OptionRefAny::None,
460                },
461                refany: RefAny::new(()),
462            }].into()));
463    }
464
465    if buttons.has_maximize {
466        let classes = IdOrClassVec::from_vec(vec![
467            Id("csd-button-maximize".into()),
468            Class("csd-button".into()),
469            Class("csd-maximize".into()),
470        ]);
471        children.push(Dom::create_div()
472            .with_ids_and_classes(classes)
473            .with_child(Dom::create_icon("maximize"))
474            .with_callbacks(vec![CoreCallbackData {
475                event: EventFilter::Hover(HoverEventFilter::MouseDown),
476                callback: CoreCallback {
477                    cb: self::callbacks::csd_maximize as usize,
478                    ctx: azul_core::refany::OptionRefAny::None,
479                },
480                refany: RefAny::new(()),
481            }].into()));
482    }
483
484    if buttons.has_close {
485        let classes = IdOrClassVec::from_vec(vec![
486            Id("csd-button-close".into()),
487            Class("csd-button".into()),
488            Class("csd-close".into()),
489        ]);
490        children.push(Dom::create_div()
491            .with_ids_and_classes(classes)
492            .with_child(Dom::create_icon("close"))
493            .with_callbacks(vec![CoreCallbackData {
494                event: EventFilter::Hover(HoverEventFilter::MouseDown),
495                callback: CoreCallback {
496                    cb: self::callbacks::csd_close as usize,
497                    ctx: azul_core::refany::OptionRefAny::None,
498                },
499                refany: RefAny::new(()),
500            }].into()));
501    }
502
503    let classes = IdOrClassVec::from_vec(vec![Class("csd-buttons".into())]);
504    Dom::create_div()
505        .with_ids_and_classes(classes)
506        .with_children(DomVec::from_vec(children))
507}
508
509impl From<Titlebar> for Dom {
510    fn from(t: Titlebar) -> Dom { t.dom() }
511}
512
513impl Default for Titlebar {
514    fn default() -> Self {
515        Titlebar::new(AzString::from_const_str(""))
516    }
517}
518
519// ── Titlebar callbacks ───────────────────────────────────────────────────
520
521/// All titlebar callbacks: drag, double-click, close, minimize, maximize.
522///
523/// Every callback is a plain `extern "C"` function that uses
524/// `CallbackInfo::modify_window_state()`.  No special hooks needed.
525pub(crate) mod callbacks {
526    use azul_core::callbacks::Update;
527    use azul_core::refany::RefAny;
528    use crate::callbacks::CallbackInfo;
529
530    /// DragStart — on Wayland, initiate compositor-managed move immediately.
531    /// On other platforms, just acknowledge (movement happens in titlebar_drag).
532    pub extern "C" fn titlebar_drag_start(
533        _data: RefAny, mut info: CallbackInfo,
534    ) -> Update {
535        // On Wayland, window position is Uninitialized (compositor hides it).
536        // We must use xdg_toplevel_move via begin_interactive_move().
537        let ws = info.get_current_window_state();
538        if matches!(ws.position, azul_core::window::WindowPosition::Uninitialized) {
539            info.begin_interactive_move();
540        }
541        Update::DoNothing
542    }
543
544    /// Drag — apply incremental screen-space delta to the CURRENT window position.
545    ///
546    /// Uses `get_drag_delta_screen_incremental()` (frame-to-frame delta) instead of
547    /// `get_drag_delta_screen()` (total delta since drag start). Combined with
548    /// the current window position from the OS, this approach is robust against
549    /// external position changes during the drag (DPI change, OS clamping,
550    /// compositor resize).
551    ///
552    /// On Wayland: this is a no-op because the compositor manages the move
553    /// (initiated by `begin_interactive_move()` in `titlebar_drag_start`).
554    pub extern "C" fn titlebar_drag(
555        _data: RefAny, mut info: CallbackInfo,
556    ) -> Update {
557        use azul_core::window::WindowPosition;
558        use azul_core::geom::PhysicalPositionI32;
559
560        let delta = info.get_drag_delta_screen_incremental();
561        let current_pos = info.get_current_window_state().position;
562
563        if let (azul_core::geom::OptionDragDelta::Some(d), WindowPosition::Initialized(pos)) = (delta, current_pos) {
564            let new_pos = WindowPosition::Initialized(PhysicalPositionI32::new(
565                pos.x + d.dx as i32,
566                pos.y + d.dy as i32,
567            ));
568            let mut ws = info.get_current_window_state().clone();
569            ws.position = new_pos;
570            info.modify_window_state(ws);
571        }
572        // On Wayland: current_pos is Uninitialized, so the if-let doesn't match → no-op.
573        Update::DoNothing
574    }
575
576    /// DoubleClick — toggle Maximized ↔ Normal.
577    pub extern "C" fn titlebar_double_click(
578        _data: RefAny, mut info: CallbackInfo,
579    ) -> Update {
580        use azul_core::window::WindowFrame;
581        let mut s = info.get_current_window_state().clone();
582        s.flags.frame = if s.flags.frame == WindowFrame::Maximized {
583            WindowFrame::Normal } else { WindowFrame::Maximized };
584        info.modify_window_state(s);
585        Update::DoNothing
586    }
587
588    /// Close button — `close_requested = true`.
589    pub extern "C" fn csd_close(
590        _data: RefAny, mut info: CallbackInfo,
591    ) -> Update {
592        let mut s = info.get_current_window_state().clone();
593        s.flags.close_requested = true;
594        info.modify_window_state(s);
595        Update::DoNothing
596    }
597
598    /// Minimize button — `frame = Minimized`.
599    pub extern "C" fn csd_minimize(
600        _data: RefAny, mut info: CallbackInfo,
601    ) -> Update {
602        use azul_core::window::WindowFrame;
603        let mut s = info.get_current_window_state().clone();
604        s.flags.frame = WindowFrame::Minimized;
605        info.modify_window_state(s);
606        Update::DoNothing
607    }
608
609    /// Maximize button — toggle Maximized ↔ Normal.
610    pub extern "C" fn csd_maximize(
611        _data: RefAny, mut info: CallbackInfo,
612    ) -> Update {
613        use azul_core::window::WindowFrame;
614        let mut s = info.get_current_window_state().clone();
615        s.flags.frame = if s.flags.frame == WindowFrame::Maximized {
616            WindowFrame::Normal } else { WindowFrame::Maximized };
617        info.modify_window_state(s);
618        Update::DoNothing
619    }
620}
621