use crate::render::{RenderContext, TextAlign, TextBaseline};
use crate::types::{IconId, Rect, WidgetState};
use super::state::SplitButtonHoverZone;
use super::theme::ButtonTheme;
use super::settings::ButtonSettings;
use super::style::DropdownMenuRowStyle;
pub struct ButtonView<'a> {
pub icon: Option<&'a IconId>,
pub text: Option<&'a str>,
pub active: bool,
pub disabled: bool,
pub active_border: Option<bool>,
pub hover_chevron: Option<HoverChevronSpec>,
}
#[derive(Debug, Clone, Copy)]
pub struct HoverChevronSpec {
pub direction: super::types::ChevronDirection,
pub inset: f64,
pub size: f64,
}
impl Default for HoverChevronSpec {
fn default() -> Self {
Self {
direction: super::types::ChevronDirection::Down,
inset: 6.0,
size: 14.0,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ButtonResult {
pub clicked: bool,
pub hovered: bool,
pub pressed: bool,
}
pub fn measure_button(view: &ButtonView<'_>, settings: &ButtonSettings) -> (f64, f64) {
let style = settings.style.as_ref();
let pad_x = style.padding_x();
let pad_y = style.padding_y();
let icon_sz = style.icon_size();
let font_sz = style.font_size();
let gap = style.gap();
let icon_w = if view.icon.is_some() { icon_sz + gap } else { 0.0 };
let text_w = view.text.map(|t| t.len() as f64 * 7.0).unwrap_or(0.0);
let content_w = icon_w + text_w;
let content_h = if view.icon.is_some() { icon_sz.max(font_sz) } else { font_sz };
let w = pad_x * 2.0 + content_w;
let h = pad_y * 2.0 + content_h;
(w, h)
}
pub fn draw_button<F>(
ctx: &mut dyn RenderContext,
rect: Rect,
state: WidgetState,
view: &ButtonView<'_>,
settings: &ButtonSettings,
draw_icon: F,
) -> ButtonResult
where
F: FnOnce(&mut dyn RenderContext, &IconId, Rect, &str),
{
let style = settings.style.as_ref();
let theme = settings.theme.as_ref();
let effective = if view.disabled {
WidgetState::Disabled
} else {
state
};
let (bg, text_color) = match effective {
WidgetState::Disabled => (theme.button_bg_disabled(), theme.button_text_disabled()),
WidgetState::Pressed => (theme.button_bg_pressed(), theme.button_text_hover()),
WidgetState::Hovered => (theme.button_bg_hover(), theme.button_text_hover()),
WidgetState::Active | WidgetState::Toggled => {
(theme.button_accent(), theme.button_text_hover())
}
WidgetState::Normal => {
if view.active {
(theme.button_accent(), theme.button_text_hover())
} else {
(theme.button_bg_normal(), theme.button_text_normal())
}
}
};
let radius = style.radius();
ctx.set_fill_color(bg);
ctx.fill_rounded_rect(rect.x, rect.y, rect.width, rect.height, radius);
let show_border = view.active_border.unwrap_or_else(|| style.show_active_border());
if view.active && show_border {
ctx.set_stroke_color(theme.button_accent());
ctx.set_stroke_width(style.border_width());
ctx.stroke_rounded_rect(rect.x, rect.y, rect.width, rect.height, radius);
}
let padding = style.padding_x().min(style.padding_y());
let content_rect = Rect::new(
rect.x + padding,
rect.y + padding,
rect.width - padding * 2.0,
rect.height - padding * 2.0,
);
if let Some(icon) = view.icon {
let icon_size = style.icon_size();
let icon_rect = Rect::new(
content_rect.x,
content_rect.y + content_rect.height / 2.0 - icon_size / 2.0,
icon_size,
icon_size,
);
draw_icon(ctx, icon, icon_rect, text_color);
if let Some(text) = view.text {
let text_area_x = icon_rect.x + icon_rect.width + style.gap();
let text_area_right = content_rect.x + content_rect.width;
let text_center_x = (text_area_x + text_area_right) / 2.0;
ctx.set_font(&format!("{}px sans-serif", style.font_size()));
ctx.set_fill_color(text_color);
ctx.set_text_align(TextAlign::Center);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(text, text_center_x, rect.y + rect.height / 2.0);
}
} else if let Some(text) = view.text {
ctx.set_font(&format!("{}px sans-serif", style.font_size()));
ctx.set_fill_color(text_color);
ctx.set_text_align(TextAlign::Center);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(text, rect.x + rect.width / 2.0, rect.y + rect.height / 2.0);
}
if let Some(spec) = view.hover_chevron {
if effective.is_hovered() && !view.disabled {
use crate::ui::widgets::atomic::chevron::{
draw_chevron,
settings::ChevronSettings,
types::{ChevronDirection as ChDir, ChevronUseCase, ChevronView, ChevronVisualKind, HitAreaPolicy, PlacementPolicy, VisibilityPolicy},
};
let chev_dir = match spec.direction {
super::types::ChevronDirection::Up => ChDir::Up,
super::types::ChevronDirection::Down => ChDir::Down,
super::types::ChevronDirection::Left => ChDir::Left,
super::types::ChevronDirection::Right => ChDir::Right,
};
let cx = rect.x + rect.width - spec.inset - spec.size / 2.0;
let cy = rect.y + rect.height / 2.0;
let chev_rect = Rect::new(cx - spec.size / 2.0, cy - spec.size / 2.0, spec.size, spec.size);
let chev_view = ChevronView {
direction: chev_dir,
use_case: ChevronUseCase::Affordance,
visibility: VisibilityPolicy::Always,
placement: PlacementPolicy::InlineCorner { trailing: true },
hit_area: HitAreaPolicy::None,
visual_kind: ChevronVisualKind::Stroked,
hovered: true,
..Default::default()
};
draw_chevron(ctx, chev_rect, &chev_view, &ChevronSettings::default());
}
}
ButtonResult {
clicked: matches!(effective, WidgetState::Pressed),
hovered: effective.is_hovered(),
pressed: effective.is_pressed(),
}
}
pub struct ColorButtonView<'a> {
pub icon: &'a IconId,
pub color: &'a str,
pub active: bool,
pub hovered: bool,
}
pub struct LineWidthButtonView {
pub width: u32,
pub active: bool,
pub hovered: bool,
}
pub struct DropdownTriggerView<'a> {
pub icon: Option<&'a IconId>,
pub text: Option<&'a str>,
pub active: bool,
pub hovered: bool,
pub show_chevron: bool,
}
pub struct SplitIconButtonView<'a> {
pub icon: &'a IconId,
pub active: bool,
pub hover_zone: SplitButtonHoverZone,
}
pub struct SplitLineWidthButtonView {
pub width: u32,
pub active: bool,
pub hover_zone: SplitButtonHoverZone,
}
#[derive(Debug, Clone)]
pub struct ToolbarHitRect {
pub id: String,
pub rect: Rect,
}
#[derive(Debug, Default, Clone)]
pub struct PanelToolbarResult {
pub item_rects: Vec<ToolbarHitRect>,
}
#[inline]
fn toolbar_pick_color<'t>(
is_hovered: bool,
is_active: bool,
theme: &'t dyn ButtonTheme,
) -> &'t str {
if is_active { theme.toolbar_item_text_active() }
else if is_hovered { theme.toolbar_item_text_hover() }
else { theme.toolbar_item_text() }
}
#[inline]
fn toolbar_draw_item_bg(
ctx: &mut dyn RenderContext,
rect: Rect,
is_hovered: bool,
is_active: bool,
theme: &dyn ButtonTheme,
) {
if is_active {
ctx.draw_active_rounded_rect(
rect.x, rect.y, rect.width, rect.height,
4.0, theme.toolbar_item_bg_active(),
);
} else if is_hovered {
ctx.draw_hover_rounded_rect(
rect.x, rect.y, rect.width, rect.height,
4.0, theme.toolbar_item_bg_hover(),
);
}
}
#[inline]
fn toolbar_draw_tiny_chevron(
ctx: &mut dyn RenderContext,
cx: f64,
cy: f64,
color: &str,
) {
let cs = 2.5_f64;
ctx.set_stroke_color(color);
ctx.set_stroke_width(1.2);
ctx.set_line_dash(&[]);
ctx.begin_path();
ctx.move_to(cx - cs, cy - cs / 2.0);
ctx.line_to(cx, cy + cs / 2.0);
ctx.line_to(cx + cs, cy - cs / 2.0);
ctx.stroke();
}
#[inline]
fn toolbar_draw_filled_chevron(
ctx: &mut dyn RenderContext,
item_rect: Rect,
color: &str,
) {
let chevron_x = item_rect.right() - 10.0;
let chevron_y = item_rect.center_y();
let chevron_size = 4.0_f64;
ctx.set_fill_color(color);
ctx.begin_path();
ctx.move_to(chevron_x - chevron_size, chevron_y - chevron_size / 2.0);
ctx.line_to(chevron_x, chevron_y + chevron_size / 2.0);
ctx.line_to(chevron_x + chevron_size, chevron_y - chevron_size / 2.0);
ctx.close_path();
ctx.fill();
}
#[inline]
fn panel_toolbar_draw_chevron(
ctx: &mut dyn RenderContext,
item_rect: Rect,
color: &str,
) {
let cx = item_rect.right() - 8.0;
let cy = item_rect.center_y();
let half = 3.0_f64;
ctx.set_stroke_color(color);
ctx.set_stroke_width(1.5);
ctx.set_line_dash(&[]);
ctx.begin_path();
ctx.move_to(cx - half, cy - 1.5);
ctx.line_to(cx, cy + 1.5);
ctx.line_to(cx + half, cy - 1.5);
ctx.stroke();
}
#[inline]
fn panel_toolbar_draw_label(
ctx: &mut dyn RenderContext,
text: &str,
item_rect: Rect,
color: &str,
has_icon: bool,
) {
ctx.set_fill_color(color);
ctx.set_font("11px sans-serif");
let x = if has_icon {
item_rect.x + item_rect.height + 2.0
} else {
item_rect.center_x()
};
ctx.fill_text(text, x, item_rect.center_y() + 4.0);
}
#[inline]
fn toolbar_draw_line_and_number(
ctx: &mut dyn RenderContext,
item_rect: Rect,
width: u32,
color: &str,
) {
let thickness = (width as f64).clamp(1.0, 4.0);
ctx.set_stroke_color(color);
ctx.set_stroke_width(thickness);
ctx.set_line_dash(&[]);
ctx.begin_path();
ctx.move_to(item_rect.x + 4.0, item_rect.center_y());
ctx.line_to(item_rect.x + 16.0, item_rect.center_y());
ctx.stroke();
ctx.set_font("12px sans-serif");
ctx.set_fill_color(color);
ctx.set_text_align(TextAlign::Left);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(&format!("{width}"), item_rect.x + 20.0, item_rect.center_y());
}
pub fn draw_toolbar_icon_button<F>(
ctx: &mut dyn RenderContext,
item_rect: Rect,
icon: &IconId,
active: bool,
disabled: bool,
hovered: bool,
is_sidebar: bool,
icon_size: f64,
theme: &dyn ButtonTheme,
draw_icon: F,
)
where
F: FnOnce(&mut dyn RenderContext, &IconId, Rect, &str),
{
let color = if disabled {
theme.toolbar_item_text()
} else if active {
if is_sidebar {
ctx.draw_sidebar_active_item(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
theme.toolbar_accent(), theme.toolbar_item_bg_active(), 3.0,
);
} else {
ctx.draw_active_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_active(),
);
}
theme.toolbar_item_text_active()
} else if hovered {
ctx.draw_hover_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_hover(),
);
theme.toolbar_item_text_hover()
} else {
theme.toolbar_item_text()
};
let icon_rect = Rect::new(
item_rect.center_x() - icon_size / 2.0,
item_rect.center_y() - icon_size / 2.0,
icon_size,
icon_size,
);
draw_icon(ctx, icon, icon_rect, color);
}
pub fn draw_toolbar_button<F>(
ctx: &mut dyn RenderContext,
item_rect: Rect,
icon: Option<&IconId>,
text: Option<&str>,
active: bool,
disabled: bool,
hovered: bool,
icon_size: f64,
theme: &dyn ButtonTheme,
draw_icon: F,
)
where
F: FnOnce(&mut dyn RenderContext, &IconId, Rect, &str),
{
let color = if disabled {
theme.toolbar_item_text()
} else if active {
ctx.draw_active_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_active(),
);
theme.toolbar_item_text_active()
} else if hovered {
ctx.draw_hover_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_hover(),
);
theme.toolbar_item_text_hover()
} else {
theme.toolbar_item_text()
};
if let Some(ic) = icon {
let icon_rect = Rect::new(
item_rect.center_x() - icon_size / 2.0,
item_rect.center_y() - icon_size / 2.0,
icon_size,
icon_size,
);
draw_icon(ctx, ic, icon_rect, color);
}
if let Some(label) = text {
let (text_x, align) = if icon.is_some() {
(item_rect.x + icon_size + 4.0, TextAlign::Left)
} else {
(item_rect.center_x(), TextAlign::Center)
};
ctx.set_font("13px sans-serif");
ctx.set_fill_color(color);
ctx.set_text_align(align);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(label, text_x, item_rect.center_y());
}
}
pub fn draw_toolbar_dropdown_trigger<F>(
ctx: &mut dyn RenderContext,
item_rect: Rect,
view: &DropdownTriggerView<'_>,
icon_size: f64,
theme: &dyn ButtonTheme,
draw_icon: F,
)
where
F: FnOnce(&mut dyn RenderContext, &IconId, Rect, &str),
{
let color = toolbar_pick_color(view.hovered, view.active, theme);
if view.active {
ctx.draw_active_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_active(),
);
} else if view.hovered {
ctx.draw_hover_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_hover(),
);
}
if let Some(ic) = view.icon {
let icon_rect = Rect::new(
item_rect.x + 4.0,
item_rect.center_y() - icon_size / 2.0,
icon_size,
icon_size,
);
draw_icon(ctx, ic, icon_rect, color);
}
if let Some(label) = view.text {
let text_x = item_rect.x + icon_size + 8.0;
ctx.set_font("13px sans-serif");
ctx.set_fill_color(color);
ctx.set_text_align(TextAlign::Left);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(label, text_x, item_rect.center_y());
}
if view.show_chevron {
toolbar_draw_filled_chevron(ctx, item_rect, color);
}
}
pub fn draw_toolbar_color_button<F>(
ctx: &mut dyn RenderContext,
item_rect: Rect,
view: &ColorButtonView<'_>,
icon_size: f64,
theme: &dyn ButtonTheme,
draw_icon: F,
)
where
F: FnOnce(&mut dyn RenderContext, &IconId, Rect, &str),
{
let color = toolbar_pick_color(view.hovered, view.active, theme);
toolbar_draw_item_bg(ctx, item_rect, view.hovered, view.active, theme);
let icon_rect = Rect::new(
item_rect.center_x() - icon_size / 2.0,
item_rect.y + 2.0, icon_size,
icon_size,
);
draw_icon(ctx, view.icon, icon_rect, color);
ctx.set_fill_color(view.color);
ctx.fill_rect(
item_rect.x + 4.0,
item_rect.bottom() - 6.0,
item_rect.width - 8.0,
3.0,
);
}
pub fn draw_toolbar_line_width_button(
ctx: &mut dyn RenderContext,
item_rect: Rect,
view: &LineWidthButtonView,
theme: &dyn ButtonTheme,
) {
let color = toolbar_pick_color(view.hovered, view.active, theme);
toolbar_draw_item_bg(ctx, item_rect, view.hovered, view.active, theme);
toolbar_draw_line_and_number(ctx, item_rect, view.width, color);
}
pub fn draw_toolbar_split_icon_button<F>(
ctx: &mut dyn RenderContext,
item_rect: Rect,
view: &SplitIconButtonView<'_>,
icon_size: f64,
theme: &dyn ButtonTheme,
draw_icon: F,
) -> (Rect, Rect)
where
F: FnOnce(&mut dyn RenderContext, &IconId, Rect, &str),
{
const CHEVRON_W: f64 = 10.0;
let main_w = item_rect.width - CHEVRON_W;
let main_rect = Rect::new(item_rect.x, item_rect.y, main_w, item_rect.height);
let chev_rect = Rect::new(item_rect.x + main_w, item_rect.y, CHEVRON_W, item_rect.height);
let is_any_hovered = view.hover_zone != SplitButtonHoverZone::None;
let color = toolbar_pick_color(is_any_hovered, view.active, theme);
if view.active {
ctx.draw_active_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_active(),
);
} else if is_any_hovered {
ctx.draw_hover_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_hover(),
);
}
let effective_icon_size = icon_size.min(main_w - 2.0);
let icon_rect = Rect::new(
main_rect.center_x() - effective_icon_size / 2.0,
main_rect.center_y() - effective_icon_size / 2.0,
effective_icon_size,
effective_icon_size,
);
draw_icon(ctx, view.icon, icon_rect, color);
toolbar_draw_tiny_chevron(ctx, chev_rect.center_x(), chev_rect.center_y(), color);
(main_rect, chev_rect)
}
pub fn draw_toolbar_split_line_width_button(
ctx: &mut dyn RenderContext,
item_rect: Rect,
view: &SplitLineWidthButtonView,
theme: &dyn ButtonTheme,
) -> (Rect, Rect) {
const CHEVRON_W: f64 = 10.0;
let main_w = item_rect.width - CHEVRON_W;
let main_rect = Rect::new(item_rect.x, item_rect.y, main_w, item_rect.height);
let chev_rect = Rect::new(item_rect.x + main_w, item_rect.y, CHEVRON_W, item_rect.height);
let is_any_hovered = view.hover_zone != SplitButtonHoverZone::None;
let color = toolbar_pick_color(is_any_hovered, view.active, theme);
if view.active {
ctx.draw_active_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_active(),
);
} else if is_any_hovered {
ctx.draw_hover_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_hover(),
);
}
toolbar_draw_line_and_number(ctx, main_rect, view.width, color);
toolbar_draw_tiny_chevron(ctx, chev_rect.center_x(), chev_rect.center_y(), color);
(main_rect, chev_rect)
}
pub enum PanelToolbarItem<'a> {
Separator,
Spacer(f64),
IconButton {
id: &'a str,
icon: &'a IconId,
active: bool,
hovered: bool,
},
Button {
id: &'a str,
icon: Option<&'a IconId>,
text: Option<&'a str>,
active: bool,
hovered: bool,
min_width: f64,
},
Dropdown {
id: &'a str,
icon: Option<&'a IconId>,
text: Option<&'a str>,
active: bool,
hovered: bool,
show_chevron: bool,
min_width: f64,
},
ColorButton {
id: &'a str,
icon: &'a IconId,
color: &'a str,
active: bool,
hovered: bool,
},
LineWidthButton {
id: &'a str,
width: u32,
active: bool,
hovered: bool,
},
SplitIconButton {
id: &'a str,
icon: &'a IconId,
active: bool,
hover_zone: SplitButtonHoverZone,
},
SplitLineWidthButton {
id: &'a str,
width: u32,
active: bool,
hover_zone: SplitButtonHoverZone,
},
Clock {
id: &'a str,
time: &'a str,
hovered: bool,
},
Label {
id: &'a str,
text: &'a str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PanelToolbarOrientation {
Horizontal,
Vertical,
}
#[allow(clippy::too_many_arguments)]
pub fn draw_panel_toolbar<'a, F>(
ctx: &mut dyn RenderContext,
rect: Rect,
items: &[PanelToolbarItem<'a>],
orientation: PanelToolbarOrientation,
item_size: f64,
icon_size: f64,
spacing: f64,
padding: f64,
theme: &dyn ButtonTheme,
mut draw_icon: F,
) -> PanelToolbarResult
where
F: FnMut(&mut dyn RenderContext, &IconId, Rect, &str),
{
let mut result = PanelToolbarResult::default();
ctx.set_fill_color(theme.toolbar_background());
ctx.fill_rect(rect.x, rect.y, rect.width, rect.height);
let is_vertical = orientation == PanelToolbarOrientation::Vertical;
let mut pos = if is_vertical {
rect.y + padding
} else {
rect.x + padding
};
for item in items {
match item {
PanelToolbarItem::Separator => {
ctx.set_stroke_color(theme.toolbar_separator());
ctx.set_stroke_width(1.0);
ctx.set_line_dash(&[]);
ctx.begin_path();
if is_vertical {
let margin = rect.width * 0.2;
ctx.move_to(rect.x + margin, pos + 3.0);
ctx.line_to(rect.right() - margin, pos + 3.0);
} else {
let margin = rect.height * 0.2;
ctx.move_to(pos + 3.0, rect.y + margin);
ctx.line_to(pos + 3.0, rect.bottom() - margin);
}
ctx.stroke();
pos += 6.0; }
PanelToolbarItem::Spacer(w) => {
pos += w;
}
PanelToolbarItem::IconButton { id, icon, active, hovered } => {
let item_rect = make_panel_item_rect(is_vertical, rect, pos, item_size, padding);
let color = toolbar_pick_color(*hovered, *active, theme);
toolbar_draw_item_bg(ctx, item_rect, *hovered, *active, theme);
render_icon_centered(ctx, icon, item_rect, icon_size, color, &mut draw_icon);
push_hit_rect(&mut result, id, item_rect);
pos += item_size + spacing;
}
PanelToolbarItem::Button { id, icon, text, active, hovered, min_width } => {
let natural_w = if *min_width > 0.0 {
*min_width
} else if text.is_some() {
item_size * 2.5
} else {
item_size
};
let item_rect = make_panel_item_rect(is_vertical, rect, pos, natural_w, padding);
let color = toolbar_pick_color(*hovered, *active, theme);
toolbar_draw_item_bg(ctx, item_rect, *hovered, *active, theme);
if let Some(ic) = icon {
render_icon_centered(ctx, ic, item_rect, icon_size, color, &mut draw_icon);
}
if let Some(label) = text {
panel_toolbar_draw_label(ctx, label, item_rect, color, icon.is_some());
}
push_hit_rect(&mut result, id, item_rect);
pos += (if is_vertical { item_size } else { natural_w }) + spacing;
}
PanelToolbarItem::Dropdown { id, icon, text, active, hovered, show_chevron, min_width } => {
let natural_w = if *min_width > 0.0 {
*min_width
} else if text.is_some() {
item_size * 2.5
} else {
item_size
};
let item_rect = make_panel_item_rect(is_vertical, rect, pos, natural_w, padding);
let color = toolbar_pick_color(*hovered, *active, theme);
toolbar_draw_item_bg(ctx, item_rect, *hovered, *active, theme);
if let Some(ic) = icon {
render_icon_centered(ctx, ic, item_rect, icon_size, color, &mut draw_icon);
}
if let Some(label) = text {
panel_toolbar_draw_label(ctx, label, item_rect, color, icon.is_some());
}
if *show_chevron && text.is_some() {
panel_toolbar_draw_chevron(ctx, item_rect, color);
}
push_hit_rect(&mut result, id, item_rect);
pos += (if is_vertical { item_size } else { natural_w }) + spacing;
}
PanelToolbarItem::ColorButton { id, icon, color: swatch_color, active, hovered } => {
let item_rect = make_panel_item_rect(is_vertical, rect, pos, item_size, padding);
let text_color = toolbar_pick_color(*hovered, *active, theme);
toolbar_draw_item_bg(ctx, item_rect, *hovered, *active, theme);
let icon_rect = Rect::new(
item_rect.center_x() - icon_size / 2.0,
item_rect.y + 2.0,
icon_size,
icon_size,
);
draw_icon(ctx, icon, icon_rect, text_color);
ctx.set_fill_color(swatch_color);
ctx.fill_rect(
item_rect.x + 4.0,
item_rect.bottom() - 6.0,
item_rect.width - 8.0,
3.0,
);
push_hit_rect(&mut result, id, item_rect);
pos += item_size + spacing;
}
PanelToolbarItem::LineWidthButton { id, width, active, hovered } => {
let item_w = 36.0_f64;
let item_rect = make_panel_item_rect(is_vertical, rect, pos, item_w, padding);
let color = toolbar_pick_color(*hovered, *active, theme);
toolbar_draw_item_bg(ctx, item_rect, *hovered, *active, theme);
toolbar_draw_line_and_number(ctx, item_rect, *width, color);
push_hit_rect(&mut result, id, item_rect);
pos += item_w + spacing;
}
PanelToolbarItem::SplitIconButton { id, icon, active, hover_zone } => {
const CHEVRON_W: f64 = 10.0;
let main_w = item_size;
let total_w = main_w + CHEVRON_W;
let item_rect = make_panel_item_rect(is_vertical, rect, pos, total_w, padding);
let main_rect = Rect::new(item_rect.x, item_rect.y, main_w, item_rect.height);
let chev_rect = Rect::new(item_rect.x + main_w, item_rect.y, CHEVRON_W, item_rect.height);
let is_any_hovered = *hover_zone != SplitButtonHoverZone::None;
let color = toolbar_pick_color(is_any_hovered, *active, theme);
if *active {
ctx.draw_active_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_active(),
);
} else if is_any_hovered {
ctx.draw_hover_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_hover(),
);
}
let eff_icon_size = icon_size.min(main_w - 2.0);
let icon_rect = Rect::new(
main_rect.center_x() - eff_icon_size / 2.0,
main_rect.center_y() - eff_icon_size / 2.0,
eff_icon_size,
eff_icon_size,
);
draw_icon(ctx, icon, icon_rect, color);
toolbar_draw_tiny_chevron(ctx, chev_rect.center_x(), chev_rect.center_y(), color);
push_hit_rect(&mut result, id, main_rect);
push_hit_rect(&mut result, &format!("{id}_menu"), chev_rect);
pos += total_w + spacing;
}
PanelToolbarItem::SplitLineWidthButton { id, width, active, hover_zone } => {
const CHEVRON_W: f64 = 10.0;
let main_w = 36.0_f64;
let total_w = main_w + CHEVRON_W;
let item_rect = make_panel_item_rect(is_vertical, rect, pos, total_w, padding);
let main_rect = Rect::new(item_rect.x, item_rect.y, main_w, item_rect.height);
let chev_rect = Rect::new(item_rect.x + main_w, item_rect.y, CHEVRON_W, item_rect.height);
let is_any_hovered = *hover_zone != SplitButtonHoverZone::None;
let color = toolbar_pick_color(is_any_hovered, *active, theme);
if *active {
ctx.draw_active_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_active(),
);
} else if is_any_hovered {
ctx.draw_hover_rounded_rect(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
4.0, theme.toolbar_item_bg_hover(),
);
}
toolbar_draw_line_and_number(ctx, main_rect, *width, color);
toolbar_draw_tiny_chevron(ctx, chev_rect.center_x(), chev_rect.center_y(), color);
push_hit_rect(&mut result, id, main_rect);
push_hit_rect(&mut result, &format!("{id}_menu"), chev_rect);
pos += total_w + spacing;
}
PanelToolbarItem::Clock { id, time, hovered } => {
let clock_w = 140.0_f64;
let item_rect = if is_vertical {
Rect::new(rect.x, pos, rect.width, item_size)
} else {
Rect::new(pos, rect.y, clock_w, rect.height)
};
if *hovered {
ctx.set_fill_color(theme.toolbar_item_bg_hover());
ctx.fill_rounded_rect(
item_rect.x,
item_rect.y + 2.0,
item_rect.width,
item_rect.height - 4.0,
4.0,
);
}
ctx.set_font("13px monospace");
ctx.set_fill_color(theme.clock_text());
ctx.set_text_align(TextAlign::Right);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(time, item_rect.right() - 8.0, item_rect.center_y());
push_hit_rect(&mut result, id, item_rect);
pos += clock_w + spacing;
}
PanelToolbarItem::Label { id, text } => {
ctx.set_font("13px sans-serif");
let text_w = ctx.measure_text(text);
let item_w = text_w + 8.0;
let item_rect = if is_vertical {
Rect::new(rect.x, pos, rect.width, item_size)
} else {
Rect::new(pos, rect.y, item_w, rect.height)
};
ctx.set_fill_color(theme.toolbar_label_text());
ctx.set_text_align(TextAlign::Left);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(text, item_rect.x + 4.0, item_rect.center_y());
push_hit_rect(&mut result, id, item_rect);
pos += item_w + spacing;
}
}
}
result
}
fn make_panel_item_rect(
is_vertical: bool,
toolbar_rect: Rect,
pos: f64,
size: f64,
padding: f64,
) -> Rect {
if is_vertical {
let inset = padding * 0.5;
Rect::new(toolbar_rect.x + inset, pos, toolbar_rect.width - inset * 2.0, size)
} else {
let inset = padding * 0.5;
Rect::new(pos, toolbar_rect.y + inset, size, toolbar_rect.height - inset * 2.0)
}
}
fn render_icon_centered<F>(
ctx: &mut dyn RenderContext,
icon: &IconId,
item_rect: Rect,
icon_size: f64,
color: &str,
draw_icon: &mut F,
)
where
F: FnMut(&mut dyn RenderContext, &IconId, Rect, &str),
{
let ir = Rect::new(
item_rect.center_x() - icon_size / 2.0,
item_rect.center_y() - icon_size / 2.0,
icon_size,
icon_size,
);
draw_icon(ctx, icon, ir, color);
}
fn push_hit_rect(result: &mut PanelToolbarResult, id: &str, rect: Rect) {
result.item_rects.push(ToolbarHitRect { id: id.to_string(), rect });
}
pub struct PrimaryButtonView<'a> {
pub text: &'a str,
pub hovered: bool,
}
pub fn draw_primary_button(
ctx: &mut dyn RenderContext,
rect: Rect,
view: &PrimaryButtonView<'_>,
radius: f64,
theme: &dyn ButtonTheme,
) -> ButtonResult {
let bg = if view.hovered {
theme.button_primary_bg_hover()
} else {
theme.button_primary_bg()
};
ctx.set_fill_color(bg);
ctx.fill_rounded_rect(rect.x, rect.y, rect.width, rect.height, radius);
ctx.set_fill_color("#ffffff");
ctx.set_font("bold 12px sans-serif");
ctx.set_text_align(TextAlign::Center);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(view.text, rect.center_x(), rect.center_y());
ButtonResult {
clicked: false,
hovered: view.hovered,
pressed: false,
}
}
pub struct GhostOutlineButtonView<'a> {
pub text: &'a str,
pub hovered: bool,
}
pub fn draw_ghost_outline_button(
ctx: &mut dyn RenderContext,
rect: Rect,
view: &GhostOutlineButtonView<'_>,
radius: f64,
theme: &dyn ButtonTheme,
) -> ButtonResult {
if view.hovered {
ctx.set_fill_color(theme.toolbar_item_bg_hover());
ctx.fill_rounded_rect(rect.x, rect.y, rect.width, rect.height, radius);
}
let border_color = if view.hovered {
theme.toolbar_item_text()
} else {
theme.toolbar_separator()
};
ctx.set_stroke_color(border_color);
ctx.set_stroke_width(1.0);
ctx.stroke_rounded_rect(rect.x, rect.y, rect.width, rect.height, radius);
let text_color = if view.hovered {
theme.toolbar_item_text_hover()
} else {
theme.toolbar_item_text()
};
ctx.set_fill_color(text_color);
ctx.set_font("13px sans-serif");
ctx.set_text_align(TextAlign::Center);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(view.text, rect.center_x(), rect.center_y());
ButtonResult {
clicked: false,
hovered: view.hovered,
pressed: false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DangerVariant {
LogOut,
Delete,
}
pub struct DangerButtonView<'a> {
pub text: &'a str,
pub hovered: bool,
pub variant: DangerVariant,
pub icon: Option<&'a IconId>,
}
pub fn draw_danger_button<F>(
ctx: &mut dyn RenderContext,
rect: Rect,
view: &DangerButtonView<'_>,
radius: f64,
theme: &dyn ButtonTheme,
draw_icon: F,
) -> ButtonResult
where
F: FnOnce(&mut dyn RenderContext, &IconId, Rect, &str),
{
let bg = if view.hovered {
theme.button_danger_bg_hover()
} else {
theme.button_danger_bg()
};
ctx.set_fill_color(bg);
ctx.fill_rounded_rect(rect.x, rect.y, rect.width, rect.height, radius);
if view.variant == DangerVariant::Delete {
let border = if view.hovered {
theme.button_danger_border_hover()
} else {
theme.button_danger_border()
};
ctx.set_stroke_color(border);
ctx.set_stroke_width(1.0);
ctx.stroke_rounded_rect(rect.x, rect.y, rect.width, rect.height, radius);
}
let text_color = theme.button_danger_text();
let text_x = if let Some(icon) = view.icon {
let icon_size = 14.0_f64;
let icon_rect = Rect::new(
rect.x + 10.0,
rect.center_y() - icon_size / 2.0,
icon_size,
icon_size,
);
draw_icon(ctx, icon, icon_rect, text_color);
icon_rect.x + icon_rect.width + 6.0
} else {
let _ = draw_icon;
rect.center_x()
};
let (align, font) = if view.icon.is_some() {
(TextAlign::Left, "bold 11px sans-serif")
} else {
(TextAlign::Center, "11px sans-serif")
};
ctx.set_fill_color(text_color);
ctx.set_font(font);
ctx.set_text_align(align);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(view.text, text_x, rect.center_y());
ButtonResult {
clicked: false,
hovered: view.hovered,
pressed: false,
}
}
pub struct SecondaryNeutralButtonView<'a> {
pub text: &'a str,
pub hovered: bool,
}
pub fn draw_secondary_neutral_button(
ctx: &mut dyn RenderContext,
rect: Rect,
view: &SecondaryNeutralButtonView<'_>,
radius: f64,
font: &str,
theme: &dyn ButtonTheme,
) -> ButtonResult {
let bg = if view.hovered {
theme.button_secondary_hover_bg()
} else {
theme.toolbar_item_bg_hover()
};
ctx.set_fill_color(bg);
ctx.fill_rounded_rect(rect.x, rect.y, rect.width, rect.height, radius);
ctx.set_stroke_color(theme.toolbar_separator());
ctx.set_stroke_width(1.0);
ctx.stroke_rounded_rect(rect.x, rect.y, rect.width, rect.height, radius);
let text_color = if view.hovered {
theme.button_secondary_text()
} else {
theme.button_secondary_text_muted()
};
ctx.set_fill_color(text_color);
ctx.set_font(font);
ctx.set_text_align(TextAlign::Center);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(view.text, rect.center_x(), rect.center_y());
ButtonResult {
clicked: false,
hovered: view.hovered,
pressed: false,
}
}
pub struct SignInButtonView<'a> {
pub text: &'a str,
pub hovered: bool,
pub icon: Option<&'a IconId>,
}
pub fn draw_signin_button<F>(
ctx: &mut dyn RenderContext,
rect: Rect,
view: &SignInButtonView<'_>,
theme: &dyn ButtonTheme,
draw_icon: F,
) -> ButtonResult
where
F: FnOnce(&mut dyn RenderContext, &IconId, Rect, &str),
{
let bg = if view.hovered {
theme.button_utility_bg_hover()
} else {
theme.button_utility_bg()
};
ctx.set_fill_color(bg);
ctx.fill_rounded_rect(rect.x, rect.y, rect.width, rect.height, 4.0);
let text_color = theme.toolbar_item_text_active();
let text_x = if let Some(icon) = view.icon {
let icon_size = 16.0_f64;
let icon_rect = Rect::new(
rect.x + 10.0,
rect.center_y() - icon_size / 2.0,
icon_size,
icon_size,
);
draw_icon(ctx, icon, icon_rect, text_color);
icon_rect.x + icon_rect.width + 6.0
} else {
let _ = draw_icon;
rect.center_x()
};
let align = if view.icon.is_some() { TextAlign::Left } else { TextAlign::Center };
ctx.set_fill_color(text_color);
ctx.set_font("bold 11px sans-serif");
ctx.set_text_align(align);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(view.text, text_x, rect.center_y());
ButtonResult {
clicked: false,
hovered: view.hovered,
pressed: false,
}
}
pub struct SidebarTabView<'a> {
pub icon: &'a IconId,
pub active: bool,
}
pub fn draw_sidebar_tab_button<F>(
ctx: &mut dyn RenderContext,
item_rect: Rect,
view: &SidebarTabView<'_>,
icon_size: f64,
theme: &dyn ButtonTheme,
draw_icon: F,
)
where
F: FnOnce(&mut dyn RenderContext, &IconId, Rect, &str),
{
let icon_color = if view.active {
ctx.draw_sidebar_active_item(
item_rect.x, item_rect.y, item_rect.width, item_rect.height,
theme.toolbar_accent(), theme.toolbar_item_bg_active(), 3.0,
);
theme.toolbar_item_text_active()
} else {
theme.toolbar_item_text()
};
let icon_rect = Rect::new(
item_rect.center_x() - icon_size / 2.0,
item_rect.center_y() - icon_size / 2.0,
icon_size,
icon_size,
);
draw_icon(ctx, view.icon, icon_rect, icon_color);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HorizontalTabActiveStyle {
FillRect,
Underline,
}
pub struct HorizontalTabView<'a> {
pub label: &'a str,
pub active: bool,
pub hovered: bool,
}
pub fn draw_horizontal_tab_button(
ctx: &mut dyn RenderContext,
item_rect: Rect,
view: &HorizontalTabView<'_>,
active_style: HorizontalTabActiveStyle,
font: &str,
theme: &dyn ButtonTheme,
) {
let text_color = if view.active {
match active_style {
HorizontalTabActiveStyle::FillRect => {
ctx.set_fill_color(theme.toolbar_item_bg_active());
ctx.fill_rect(item_rect.x, item_rect.y, item_rect.width, item_rect.height);
}
HorizontalTabActiveStyle::Underline => {
const UNDERLINE_H: f64 = 2.0;
ctx.set_fill_color(theme.toolbar_accent());
ctx.fill_rect(
item_rect.x,
item_rect.bottom() - UNDERLINE_H,
item_rect.width,
UNDERLINE_H,
);
}
}
theme.toolbar_item_text_active()
} else if view.hovered {
ctx.set_fill_color(theme.toolbar_item_bg_hover());
ctx.fill_rect(item_rect.x, item_rect.y, item_rect.width, item_rect.height);
theme.toolbar_item_text_hover()
} else {
theme.toolbar_item_text()
};
ctx.set_fill_color(text_color);
ctx.set_font(font);
ctx.set_text_align(TextAlign::Center);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(view.label, item_rect.center_x(), item_rect.center_y());
}
pub struct UtilityButtonView<'a> {
pub text: &'a str,
pub hovered: bool,
pub prominent: bool,
}
pub fn draw_utility_button(
ctx: &mut dyn RenderContext,
rect: Rect,
view: &UtilityButtonView<'_>,
font: &str,
theme: &dyn ButtonTheme,
) -> ButtonResult {
let bg = if view.hovered {
theme.button_utility_bg_hover()
} else {
theme.button_utility_bg()
};
ctx.set_fill_color(bg);
ctx.fill_rounded_rect(rect.x, rect.y, rect.width, rect.height, 4.0);
let text_color = if view.hovered || view.prominent {
theme.toolbar_item_text()
} else {
theme.toolbar_item_text() };
ctx.set_fill_color(text_color);
ctx.set_font(font);
ctx.set_text_align(TextAlign::Center);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.fill_text(view.text, rect.center_x(), rect.center_y());
ButtonResult {
clicked: false,
hovered: view.hovered,
pressed: false,
}
}
pub struct DropdownMenuRowView<'a> {
pub label: &'a str,
pub icon: Option<&'a IconId>,
pub selected: bool,
pub hovered: bool,
pub separator_after: bool,
}
pub fn draw_dropdown_menu_row<F>(
ctx: &mut dyn RenderContext,
rect: Rect,
view: &DropdownMenuRowView<'_>,
font: &str,
icon_size: f64,
style: &dyn DropdownMenuRowStyle,
theme: &dyn ButtonTheme,
draw_icon: F,
)
where
F: FnOnce(&mut dyn RenderContext, &IconId, Rect, &str),
{
let ix = style.highlight_inset_x();
let r = style.radius();
let highlight_bg = if view.selected {
Some(theme.dropdown_menu_row_bg_selected())
} else if view.hovered {
Some(theme.dropdown_menu_row_bg_hover())
} else {
None
};
if let Some(bg) = highlight_bg {
ctx.set_fill_color(bg);
if r > 0.0 {
ctx.fill_rounded_rect(
rect.x + ix, rect.y,
rect.width - ix * 2.0, rect.height,
r,
);
} else {
ctx.fill_rect(
rect.x + ix, rect.y,
rect.width - ix * 2.0, rect.height,
);
}
}
let text_color = if view.selected {
theme.dropdown_menu_row_text_selected()
} else {
theme.dropdown_menu_row_text()
};
let mut text_x = rect.x + style.text_padding_x();
if let Some(icon) = view.icon {
let icon_rect = Rect::new(
text_x,
rect.center_y() - icon_size / 2.0,
icon_size,
icon_size,
);
draw_icon(ctx, icon, icon_rect, text_color);
text_x += icon_size + 6.0;
}
ctx.set_font(font);
ctx.set_text_align(TextAlign::Left);
ctx.set_text_baseline(TextBaseline::Middle);
ctx.set_fill_color(text_color);
ctx.fill_text(view.label, text_x, rect.center_y());
if view.separator_after {
let sep_y = rect.y + rect.height - style.separator_height();
ctx.set_stroke_color(theme.dropdown_menu_separator());
ctx.set_stroke_width(style.separator_height());
ctx.set_line_dash(&[]);
ctx.begin_path();
ctx.move_to(rect.x, sep_y);
ctx.line_to(rect.x + rect.width, sep_y);
ctx.stroke();
}
}