use azul_core::{
dom::{Dom, DomVec, IdOrClass, IdOrClass::Class, IdOrClass::Id, IdOrClassVec},
refany::RefAny,
};
use azul_css::{
dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec},
props::{
basic::{
color::{ColorU, ColorOrSystem, SystemColorRef},
font::{StyleFontFamily, StyleFontFamilyVec},
*,
},
layout::*,
property::{CssProperty, *},
style::*,
},
system::{SystemFontType, SystemStyle, TitlebarButtonSide, TitlebarButtons, TitlebarMetrics},
*,
};
#[cfg(target_os = "macos")]
const DEFAULT_TITLEBAR_HEIGHT: f32 = 28.0;
#[cfg(target_os = "windows")]
const DEFAULT_TITLEBAR_HEIGHT: f32 = 32.0;
#[cfg(target_os = "linux")]
const DEFAULT_TITLEBAR_HEIGHT: f32 = 30.0;
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
const DEFAULT_TITLEBAR_HEIGHT: f32 = 32.0;
#[cfg(target_os = "macos")]
const DEFAULT_TITLE_FONT_SIZE: f32 = 13.0;
#[cfg(target_os = "windows")]
const DEFAULT_TITLE_FONT_SIZE: f32 = 12.0;
#[cfg(target_os = "linux")]
const DEFAULT_TITLE_FONT_SIZE: f32 = 13.0;
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
const DEFAULT_TITLE_FONT_SIZE: f32 = 13.0;
#[cfg(target_os = "macos")]
const DEFAULT_BUTTON_AREA_WIDTH: f32 = 78.0;
#[cfg(target_os = "windows")]
const DEFAULT_BUTTON_AREA_WIDTH: f32 = 138.0;
#[cfg(target_os = "linux")]
const DEFAULT_BUTTON_AREA_WIDTH: f32 = 100.0;
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
const DEFAULT_BUTTON_AREA_WIDTH: f32 = 100.0;
#[cfg(target_os = "macos")]
const DEFAULT_BUTTON_SIDE_LEFT: bool = true;
#[cfg(not(target_os = "macos"))]
const DEFAULT_BUTTON_SIDE_LEFT: bool = false;
const 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)]
#[repr(C)]
pub struct Titlebar {
pub title: AzString,
pub height: f32,
pub font_size: f32,
pub padding_left: f32,
pub padding_right: f32,
pub title_color: ColorU,
}
impl Titlebar {
#[inline]
pub fn new(title: AzString) -> Self {
let half = DEFAULT_BUTTON_AREA_WIDTH / 2.0;
let (padding_left, padding_right) = (half, half);
Self {
title,
height: DEFAULT_TITLEBAR_HEIGHT,
font_size: DEFAULT_TITLE_FONT_SIZE,
padding_left,
padding_right,
title_color: DEFAULT_TITLE_COLOR_LIGHT,
}
}
#[inline]
pub fn create(title: AzString) -> Self {
Self::new(title)
}
#[inline]
pub fn with_height(title: AzString, height: f32) -> Self {
let mut tb = Self::new(title);
tb.height = height;
tb
}
#[inline]
pub fn set_height(&mut self, height: f32) {
self.height = height;
}
#[inline]
pub fn set_title(&mut self, title: AzString) {
self.title = title;
}
#[inline]
pub fn swap_with_default(&mut self) -> Self {
let mut s = Titlebar::new(AzString::from_const_str(""));
core::mem::swap(&mut s, self);
s
}
pub fn from_system_style(title: AzString, system_style: &SystemStyle) -> Self {
let tm = &system_style.metrics.titlebar;
let height = tm.height.as_ref()
.map(|pv| pv.to_pixels_internal(0.0, 0.0))
.unwrap_or(DEFAULT_TITLEBAR_HEIGHT);
let font_size = tm.title_font_size
.into_option()
.unwrap_or(DEFAULT_TITLE_FONT_SIZE);
let button_area = tm.button_area_width.as_ref()
.map(|pv| pv.to_pixels_internal(0.0, 0.0))
.unwrap_or(DEFAULT_BUTTON_AREA_WIDTH);
let safe_left = tm.safe_area.left.as_ref()
.map(|pv| pv.to_pixels_internal(0.0, 0.0))
.unwrap_or(0.0);
let safe_right = tm.safe_area.right.as_ref()
.map(|pv| pv.to_pixels_internal(0.0, 0.0))
.unwrap_or(0.0);
let pad_h = tm.padding_horizontal.as_ref()
.map(|pv| pv.to_pixels_internal(0.0, 0.0))
.unwrap_or(0.0);
let half_btn = button_area / 2.0;
let (padding_left, padding_right) = (
half_btn + safe_left + pad_h,
half_btn + safe_right + pad_h,
);
let title_color = system_style.colors.text.into_option().unwrap_or(
match system_style.theme {
azul_css::system::Theme::Dark => DEFAULT_TITLE_COLOR_DARK,
azul_css::system::Theme::Light => DEFAULT_TITLE_COLOR_LIGHT,
}
);
Self { title, height, font_size, padding_left, padding_right, title_color }
}
pub fn from_system_style_csd(title: AzString, system_style: &SystemStyle) -> Self {
let tm = &system_style.metrics.titlebar;
let height = tm.height.as_ref()
.map(|pv| pv.to_pixels_internal(0.0, 0.0))
.unwrap_or(DEFAULT_TITLEBAR_HEIGHT);
let font_size = tm.title_font_size
.into_option()
.unwrap_or(DEFAULT_TITLE_FONT_SIZE);
let title_color = system_style.colors.text.into_option().unwrap_or(
match system_style.theme {
azul_css::system::Theme::Dark => DEFAULT_TITLE_COLOR_DARK,
azul_css::system::Theme::Light => DEFAULT_TITLE_COLOR_LIGHT,
}
);
Self { title, height, font_size, padding_left: 0.0, padding_right: 0.0, title_color }
}
fn build_container_style(&self, show_buttons: bool) -> CssPropertyWithConditionsVec {
let mut props = Vec::with_capacity(8);
if show_buttons {
props.push(CssPropertyWithConditions::simple(
CssProperty::const_display(LayoutDisplay::Flex),
));
props.push(CssPropertyWithConditions::simple(
CssProperty::const_flex_direction(LayoutFlexDirection::Row),
));
props.push(CssPropertyWithConditions::simple(
CssProperty::const_align_items(LayoutAlignItems::Center),
));
} else {
props.push(CssPropertyWithConditions::simple(
CssProperty::const_display(LayoutDisplay::Block),
));
}
props.push(CssPropertyWithConditions::simple(
CssProperty::const_height(LayoutHeight::const_px(self.height as isize)),
));
props.push(CssPropertyWithConditions::simple(
CssProperty::const_cursor(StyleCursor::Grab),
));
props.push(CssPropertyWithConditions::simple(
CssProperty::user_select(StyleUserSelect::None),
));
if self.padding_left > 0.0 {
props.push(CssPropertyWithConditions::simple(
CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
self.padding_left as isize,
)),
));
}
if self.padding_right > 0.0 {
props.push(CssPropertyWithConditions::simple(
CssProperty::const_padding_right(LayoutPaddingRight::const_px(
self.padding_right as isize,
)),
));
}
CssPropertyWithConditionsVec::from_vec(props)
}
fn build_title_style(&self, show_buttons: bool) -> CssPropertyWithConditionsVec {
let font_family = StyleFontFamilyVec::from_vec(vec![
StyleFontFamily::SystemType(SystemFontType::TitleBold),
]);
let mut props = Vec::with_capacity(10);
props.push(CssPropertyWithConditions::simple(
CssProperty::const_font_size(StyleFontSize::const_px(self.font_size as isize)),
));
props.push(CssPropertyWithConditions::simple(
CssProperty::const_font_family(font_family),
));
props.push(CssPropertyWithConditions::simple(
CssProperty::const_text_color(StyleTextColor { inner: self.title_color }),
));
if show_buttons {
props.push(CssPropertyWithConditions::simple(
CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1)),
));
props.push(CssPropertyWithConditions::simple(
CssProperty::const_min_width(LayoutMinWidth::const_px(0)),
));
}
props.push(CssPropertyWithConditions::simple(
CssProperty::const_text_align(StyleTextAlign::Center),
));
props.push(CssPropertyWithConditions::simple(
CssProperty::WhiteSpace(StyleWhiteSpaceValue::Exact(StyleWhiteSpace::Nowrap)),
));
props.push(CssPropertyWithConditions::simple(
CssProperty::const_overflow_x(LayoutOverflow::Hidden),
));
let v_pad = ((self.height - self.font_size) / 2.0).max(0.0);
if v_pad > 0.0 {
props.push(CssPropertyWithConditions::simple(
CssProperty::const_padding_top(LayoutPaddingTop::const_px(v_pad as isize)),
));
}
CssPropertyWithConditionsVec::from_vec(props)
}
#[inline]
pub fn dom(self) -> Dom {
self.dom_inner(false, &TitlebarButtons::default(), TitlebarButtonSide::Right)
}
pub fn dom_with_buttons(
self,
buttons: &TitlebarButtons,
button_side: TitlebarButtonSide,
) -> Dom {
self.dom_inner(true, buttons, button_side)
}
fn dom_inner(
self,
show_buttons: bool,
buttons: &TitlebarButtons,
button_side: TitlebarButtonSide,
) -> Dom {
use azul_core::{
callbacks::{CoreCallback, CoreCallbackData},
dom::{EventFilter, HoverEventFilter},
};
#[derive(Debug, Clone, Copy)]
struct DragMarker;
let title_style = self.build_title_style(show_buttons);
let container_style = self.build_container_style(show_buttons);
let title_classes = IdOrClassVec::from_vec(vec![Class("csd-title".into())]);
let title_node = Dom::create_div()
.with_ids_and_classes(title_classes)
.with_css_props(title_style)
.with_child(Dom::create_text(self.title)) .with_callbacks(vec![
CoreCallbackData {
event: EventFilter::Hover(HoverEventFilter::DragStart),
callback: CoreCallback {
cb: self::callbacks::titlebar_drag_start as usize,
ctx: azul_core::refany::OptionRefAny::None,
},
refany: RefAny::new(DragMarker),
},
CoreCallbackData {
event: EventFilter::Hover(HoverEventFilter::Drag),
callback: CoreCallback {
cb: self::callbacks::titlebar_drag as usize,
ctx: azul_core::refany::OptionRefAny::None,
},
refany: RefAny::new(DragMarker),
},
CoreCallbackData {
event: EventFilter::Hover(HoverEventFilter::DoubleClick),
callback: CoreCallback {
cb: self::callbacks::titlebar_double_click as usize,
ctx: azul_core::refany::OptionRefAny::None,
},
refany: RefAny::new(DragMarker),
},
].into());
let button_container = if show_buttons {
Some(build_button_container(buttons))
} else {
None
};
let container_classes = IdOrClassVec::from_vec(vec![
Class("csd-titlebar".into()),
Class("__azul-native-titlebar".into()),
]);
let mut root = Dom::create_div()
.with_ids_and_classes(container_classes)
.with_css_props(container_style);
match button_side {
TitlebarButtonSide::Left => {
if let Some(btn) = button_container { root = root.with_child(btn); }
root = root.with_child(title_node);
}
TitlebarButtonSide::Right => {
root = root.with_child(title_node);
if let Some(btn) = button_container { root = root.with_child(btn); }
}
}
root
}
}
fn build_button_container(buttons: &TitlebarButtons) -> Dom {
use azul_core::{
callbacks::{CoreCallback, CoreCallbackData},
dom::{EventFilter, HoverEventFilter},
};
let mut children = Vec::new();
if buttons.has_minimize {
let classes = IdOrClassVec::from_vec(vec![
Id("csd-button-minimize".into()),
Class("csd-button".into()),
Class("csd-minimize".into()),
]);
children.push(Dom::create_div()
.with_ids_and_classes(classes)
.with_child(Dom::create_icon("minimize"))
.with_callbacks(vec![CoreCallbackData {
event: EventFilter::Hover(HoverEventFilter::MouseDown),
callback: CoreCallback {
cb: self::callbacks::csd_minimize as usize,
ctx: azul_core::refany::OptionRefAny::None,
},
refany: RefAny::new(()),
}].into()));
}
if buttons.has_maximize {
let classes = IdOrClassVec::from_vec(vec![
Id("csd-button-maximize".into()),
Class("csd-button".into()),
Class("csd-maximize".into()),
]);
children.push(Dom::create_div()
.with_ids_and_classes(classes)
.with_child(Dom::create_icon("maximize"))
.with_callbacks(vec![CoreCallbackData {
event: EventFilter::Hover(HoverEventFilter::MouseDown),
callback: CoreCallback {
cb: self::callbacks::csd_maximize as usize,
ctx: azul_core::refany::OptionRefAny::None,
},
refany: RefAny::new(()),
}].into()));
}
if buttons.has_close {
let classes = IdOrClassVec::from_vec(vec![
Id("csd-button-close".into()),
Class("csd-button".into()),
Class("csd-close".into()),
]);
children.push(Dom::create_div()
.with_ids_and_classes(classes)
.with_child(Dom::create_icon("close"))
.with_callbacks(vec![CoreCallbackData {
event: EventFilter::Hover(HoverEventFilter::MouseDown),
callback: CoreCallback {
cb: self::callbacks::csd_close as usize,
ctx: azul_core::refany::OptionRefAny::None,
},
refany: RefAny::new(()),
}].into()));
}
let classes = IdOrClassVec::from_vec(vec![Class("csd-buttons".into())]);
Dom::create_div()
.with_ids_and_classes(classes)
.with_children(DomVec::from_vec(children))
}
impl From<Titlebar> for Dom {
fn from(t: Titlebar) -> Dom { t.dom() }
}
impl Default for Titlebar {
fn default() -> Self {
Titlebar::new(AzString::from_const_str(""))
}
}
pub(crate) mod callbacks {
use azul_core::callbacks::Update;
use azul_core::refany::RefAny;
use crate::callbacks::CallbackInfo;
pub extern "C" fn titlebar_drag_start(
_data: RefAny, mut info: CallbackInfo,
) -> Update {
let ws = info.get_current_window_state();
if matches!(ws.position, azul_core::window::WindowPosition::Uninitialized) {
info.begin_interactive_move();
}
Update::DoNothing
}
pub extern "C" fn titlebar_drag(
_data: RefAny, mut info: CallbackInfo,
) -> Update {
use azul_core::window::WindowPosition;
use azul_core::geom::PhysicalPositionI32;
let delta = info.get_drag_delta_screen_incremental();
let current_pos = info.get_current_window_state().position;
if let (azul_core::geom::OptionDragDelta::Some(d), WindowPosition::Initialized(pos)) = (delta, current_pos) {
let new_pos = WindowPosition::Initialized(PhysicalPositionI32::new(
pos.x + d.dx as i32,
pos.y + d.dy as i32,
));
let mut ws = info.get_current_window_state().clone();
ws.position = new_pos;
info.modify_window_state(ws);
}
Update::DoNothing
}
pub extern "C" fn titlebar_double_click(
_data: RefAny, mut info: CallbackInfo,
) -> Update {
use azul_core::window::WindowFrame;
let mut s = info.get_current_window_state().clone();
s.flags.frame = if s.flags.frame == WindowFrame::Maximized {
WindowFrame::Normal } else { WindowFrame::Maximized };
info.modify_window_state(s);
Update::DoNothing
}
pub extern "C" fn csd_close(
_data: RefAny, mut info: CallbackInfo,
) -> Update {
let mut s = info.get_current_window_state().clone();
s.flags.close_requested = true;
info.modify_window_state(s);
Update::DoNothing
}
pub extern "C" fn csd_minimize(
_data: RefAny, mut info: CallbackInfo,
) -> Update {
use azul_core::window::WindowFrame;
let mut s = info.get_current_window_state().clone();
s.flags.frame = WindowFrame::Minimized;
info.modify_window_state(s);
Update::DoNothing
}
pub extern "C" fn csd_maximize(
_data: RefAny, mut info: CallbackInfo,
) -> Update {
use azul_core::window::WindowFrame;
let mut s = info.get_current_window_state().clone();
s.flags.frame = if s.flags.frame == WindowFrame::Maximized {
WindowFrame::Normal } else { WindowFrame::Maximized };
info.modify_window_state(s);
Update::DoNothing
}
}