1use 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#[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#[cfg(target_os = "macos")]
48const DEFAULT_BUTTON_AREA_WIDTH: f32 = 78.0;
49#[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#[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
63const DEFAULT_TITLE_COLOR_LIGHT: ColorU = ColorU { r: 76, g: 76, b: 76, a: 255 }; const DEFAULT_TITLE_COLOR_DARK: ColorU = ColorU { r: 229, g: 229, b: 229, a: 255 }; #[derive(Debug, Clone, PartialEq, PartialOrd)]
103#[repr(C)]
104pub struct Titlebar {
105 pub title: AzString,
107 pub height: f32,
109 pub font_size: f32,
111 pub padding_left: f32,
113 pub padding_right: f32,
115 pub title_color: ColorU,
117}
118
119impl Titlebar {
120 #[inline]
125 pub fn new(title: AzString) -> Self {
126 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 #[inline]
142 pub fn create(title: AzString) -> Self {
143 Self::new(title)
144 }
145
146 #[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 #[inline]
156 pub fn set_height(&mut self, height: f32) {
157 self.height = height;
158 }
159
160 #[inline]
162 pub fn set_title(&mut self, title: AzString) {
163 self.title = title;
164 }
165
166 #[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 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 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 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 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 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 fn build_container_style(&self, show_buttons: bool) -> CssPropertyWithConditionsVec {
239 let mut props = Vec::with_capacity(8);
240 if show_buttons {
241 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 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 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 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 props.push(CssPropertyWithConditions::simple(
299 CssProperty::const_text_color(StyleTextColor { inner: self.title_color }),
300 ));
301 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 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 #[inline]
334 pub fn dom(self) -> Dom {
335 self.dom_inner(false, &TitlebarButtons::default(), TitlebarButtonSide::Right)
336 }
337
338 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 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 let title_style = self.build_title_style(show_buttons);
367 let container_style = self.build_container_style(show_buttons);
368
369 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)) .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 let button_container = if show_buttons {
405 Some(build_button_container(buttons))
406 } else {
407 None
408 };
409
410 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 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
437fn 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
519pub(crate) mod callbacks {
526 use azul_core::callbacks::Update;
527 use azul_core::refany::RefAny;
528 use crate::callbacks::CallbackInfo;
529
530 pub extern "C" fn titlebar_drag_start(
533 _data: RefAny, mut info: CallbackInfo,
534 ) -> Update {
535 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 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 Update::DoNothing
574 }
575
576 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 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 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 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