1
//! Titlebar widget for custom window chrome (CSD and title-only modes).
2
//!
3
//! Key type: [`Titlebar`]
4

            
5
use azul_core::{
6
    dom::{Dom, DomVec, IdOrClass, IdOrClass::Class, IdOrClass::Id, IdOrClassVec},
7
    refany::RefAny,
8
};
9
use 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")]
29
const DEFAULT_TITLEBAR_HEIGHT: f32 = 28.0;
30
#[cfg(target_os = "windows")]
31
const DEFAULT_TITLEBAR_HEIGHT: f32 = 32.0;
32
#[cfg(target_os = "linux")]
33
const DEFAULT_TITLEBAR_HEIGHT: f32 = 30.0;
34
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
35
const DEFAULT_TITLEBAR_HEIGHT: f32 = 32.0;
36

            
37
#[cfg(target_os = "macos")]
38
const DEFAULT_TITLE_FONT_SIZE: f32 = 13.0;
39
#[cfg(target_os = "windows")]
40
const DEFAULT_TITLE_FONT_SIZE: f32 = 12.0;
41
#[cfg(target_os = "linux")]
42
const DEFAULT_TITLE_FONT_SIZE: f32 = 13.0;
43
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
44
const DEFAULT_TITLE_FONT_SIZE: f32 = 13.0;
45

            
46
// Verified: macOS 11–15 traffic-light geometry = 78px including gaps
47
#[cfg(target_os = "macos")]
48
const DEFAULT_BUTTON_AREA_WIDTH: f32 = 78.0;
49
// Windows 10/11: 3 buttons × 46px = 138px
50
#[cfg(target_os = "windows")]
51
const DEFAULT_BUTTON_AREA_WIDTH: f32 = 138.0;
52
#[cfg(target_os = "linux")]
53
const DEFAULT_BUTTON_AREA_WIDTH: f32 = 100.0;
54
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
55
const DEFAULT_BUTTON_AREA_WIDTH: f32 = 100.0;
56

            
57
// macOS: traffic lights on the left.  All others: right.
58
#[cfg(target_os = "macos")]
59
const DEFAULT_BUTTON_SIDE_LEFT: bool = true;
60
#[cfg(not(target_os = "macos"))]
61
const DEFAULT_BUTTON_SIDE_LEFT: bool = false;
62

            
63
// Default title text color for light / dark fallback
64
const DEFAULT_TITLE_COLOR_LIGHT: ColorU = ColorU { r: 76, g: 76, b: 76, a: 255 };  // #4c4c4c
65
const 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)]
104
pub 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

            
119
impl 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.
438
fn 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

            
509
impl From<Titlebar> for Dom {
510
    fn from(t: Titlebar) -> Dom { t.dom() }
511
}
512

            
513
impl 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.
525
pub(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