feat(ui): add configurable Iced themes

This commit is contained in:
Hakan Seven committed 2026-07-28 09:33:12 +03:00
1 parent 73545520c8
commit 4bf14baea4
52 files changed
+3063 -5670

No files matched your search

+3 -6
View File
@@ -15,7 +15,7 @@ use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, Widget};
use iced::advanced::{mouse, overlay, renderer, Clipboard, Renderer as _, Shell};
use iced::{
Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size,
Background, Border, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size,
Theme, Vector,
};
@@ -113,8 +113,6 @@ pub struct CollapsePanels<'a> {
open: Option<String>,
/// Fallback row height, used only when there are no panels to measure.
row_h: f32,
/// Colour of the 1px divider drawn between panels.
divider: Color,
/// Chosen degradation level per panel; set during layout.
levels: RefCell<Vec<u8>>,
/// If set, the measured row height is written here each layout (read when
@@ -129,13 +127,12 @@ pub struct CollapsePanels<'a> {
}
impl<'a> CollapsePanels<'a> {
pub fn new(panels: Vec<Panel<'a>>, open: Option<String>, row_h: f32, divider: Color) -> Self {
pub fn new(panels: Vec<Panel<'a>>, open: Option<String>, row_h: f32) -> Self {
let n = panels.len();
Self {
panels,
open,
row_h,
divider,
levels: RefCell::new(vec![FULL; n]),
height_out: None,
tight_out: None,
@@ -480,7 +477,7 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
shadow: Shadow::default(),
snap: true,
},
Background::Color(self.divider),
Background::Color(theme.extended_palette().background.neutral.color),
);
}
}
+152 -326
View File
@@ -12,7 +12,7 @@ use std::sync::Arc;
use rustc_hash::FxHashMap as HashMap;
use acadrust::types::{Color as AcadColor, LineWeight};
use iced::widget::{button, column, container, mouse_area, row, scrollable, svg, text};
use iced::widget::{button, column, container, mouse_area, row, scrollable, text};
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
use crate::app::Message;
@@ -419,58 +419,41 @@ impl Ribbon {
let is_active = i == self.active;
let is_contextual = module.id() == "layout";
let accent = if is_contextual {
ACCENT_GOLD
} else {
ACCENT_BLUE
};
let text_inactive = if is_contextual {
Color {
r: 0.90,
g: 0.72,
b: 0.30,
a: 1.0,
}
} else {
Color {
r: 0.75,
g: 0.75,
b: 0.75,
a: 1.0,
}
};
let hover_bg = if is_contextual {
Color {
r: 0.28,
g: 0.24,
b: 0.12,
a: 1.0,
}
} else {
Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
}
};
let btn = container(
button(text(module.title()).size(12))
.on_press(Message::RibbonSelectTab(i))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match (is_active, status) {
(true, _) => RIBBON_BG,
(false, button::Status::Hovered) => hover_bg,
_ => Color::TRANSPARENT,
})),
text_color: if is_active {
Color::WHITE
.style(move |theme: &Theme, status| {
let palette = theme.extended_palette();
let accent = if is_contextual {
palette.warning.base
} else {
text_inactive
palette.primary.base
};
let pair = match (is_active, status) {
(true, _) => palette.background.weakest,
(false, button::Status::Hovered) => {
if is_contextual {
palette.warning.weak
} else {
palette.background.weak
}
}
_ => palette.background.base,
};
button::Style {
background: (is_active
|| matches!(status, button::Status::Hovered))
.then_some(Background::Color(pair.color)),
text_color: if is_active {
pair.text
} else if is_contextual {
accent.color
} else {
palette.background.base.text.scale_alpha(0.72)
},
border: Border {
color: if is_active {
accent
accent.color
} else {
Color::TRANSPARENT
},
@@ -479,13 +462,18 @@ impl Ribbon {
},
shadow: iced::Shadow::default(),
snap: false,
}
})
.padding([5, 14]),
)
.style(move |_: &Theme| container::Style {
.style(move |theme: &Theme| container::Style {
border: Border {
color: if is_active {
accent
if is_contextual {
theme.extended_palette().warning.base.color
} else {
theme.extended_palette().primary.base.color
}
} else {
Color::TRANSPARENT
},
@@ -514,9 +502,11 @@ impl Ribbon {
// persisted (see `Ribbon::set_collapse_mode`). It hides itself once the
// tool row is tight, giving the cramped tab row its space back.
let dd_open = self.open_dropdown.as_deref() == Some(COLLAPSE_MODE_ID);
let mode_btn = button(crate::ui::icons::arrow_down(10.0, ARROW_COLOR))
let mode_btn = button(crate::ui::icons::themed_arrow_down(10.0))
.on_press(Message::ToggleRibbonDropdown(COLLAPSE_MODE_ID.to_string()))
.style(move |_: &Theme, status| top_hist_btn_style(true, dd_open, status))
.style(move |theme: &Theme, status| {
top_hist_btn_style(theme, true, dd_open, status)
})
.height(24)
.padding([2, 8]);
let mode_dd = PosReport::new(COLLAPSE_MODE_ID, mode_btn);
@@ -534,8 +524,10 @@ impl Ribbon {
}
let tab_bar = container(tab_row)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(TOPBAR_BG)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.base.color,
)),
..Default::default()
})
.padding(Padding {
@@ -653,10 +645,12 @@ impl Ribbon {
self.active_lineweight,
&style_ctx,
))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(RIBBON_BG)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.weakest.color,
)),
border: Border {
color: BORDER_DARK,
color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
@@ -666,7 +660,7 @@ impl Ribbon {
}
})
.collect();
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H, BORDER_DARK)
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
.report_height(self.tool_bar_h.clone())
.report_tight(self.collapse_tight.clone())
.mode(self.collapse_mode)
@@ -676,10 +670,12 @@ impl Ribbon {
};
let tool_bar = container(tool_area)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(RIBBON_BG)),
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.weakest.color,
)),
border: Border {
color: BORDER_DARK,
color: theme.extended_palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
@@ -712,19 +708,13 @@ impl Ribbon {
.enumerate()
.map(|(idx, label)| {
let step = idx + 1;
button(text(label.clone()).size(11).color(LABEL_ON))
button(text(label.clone()).size(11))
.on_press(if is_undo {
Message::UndoMany(step)
} else {
Message::RedoMany(step)
})
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([5, 10])
.into()
@@ -732,15 +722,7 @@ impl Ribbon {
.collect();
let panel = container(column(rows))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(170.0));
let (align_right, h_pad, top) = self.dd_anchor(open_id, 170.0, win.0);
@@ -755,27 +737,16 @@ impl Ribbon {
let rows: Vec<Element<Message>> = CollapseMode::ALL
.iter()
.map(|&m| {
let mark: Element<Message> = if m == current {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
};
button(
row![
container(mark).width(Length::Fixed(16.0)),
text(m.label()).size(11).color(LABEL_ON),
crate::ui::icons::themed_check_cell(m == current),
text(m.label()).size(11),
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::SetRibbonCollapseMode(m))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([5, 10])
.into()
@@ -783,15 +754,7 @@ impl Ribbon {
.collect();
let panel = container(column(rows))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(W));
let (align_right, h_pad, top) = self.dd_anchor(open_id, W, win.0);
@@ -852,28 +815,25 @@ impl Ribbon {
.iter()
.map(|(cmd, label, item_icon)| {
let is_current = *cmd == last_cmd;
let checkmark: Element<'_, Message> = container(if is_current {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let icon_el: Element<Message> = match *item_icon {
IconKind::Glyph(s) => text(s)
.size(13)
.color(ICON_COLOR)
let checkmark: Element<'_, Message> =
crate::ui::icons::themed_check_cell(is_current);
let icon_el: Element<Message> =
container(make_icon(*item_icon, 20.0))
.width(Length::Fixed(20.0))
.into(),
IconKind::Svg(bytes) => {
let handle = svg::Handle::from_memory(bytes);
svg(handle).width(20).height(20).into()
}
};
.into();
let label_el =
text(*label)
.size(11)
.color(if is_current { LABEL_ON } else { LABEL_OFF });
.style(move |theme: &Theme| iced::widget::text::Style {
color: (!is_current).then_some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
});
button(
row![checkmark, icon_el, label_el]
@@ -884,13 +844,7 @@ impl Ribbon {
dropdown_id: dd_id,
cmd: *cmd,
})
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([4, 10])
.into()
@@ -898,15 +852,7 @@ impl Ribbon {
.collect();
let panel = container(column(rows))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(190.0));
let (align_right, h_pad, top) = self.dd_anchor(open_id, 190.0, win.0);
@@ -925,13 +871,7 @@ impl Ribbon {
let icon_btn = |bytes: &'static [u8], msg: Message| -> Element<'_, Message> {
button(crate::ui::icons::raw(bytes, 14.0))
.on_press(msg)
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.padding([2, 4])
.into()
};
@@ -952,10 +892,10 @@ impl Ribbon {
let name = info.name.clone();
let swatch = container(text(""))
.style(move |_: &Theme| container::Style {
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(lc)),
border: Border {
color: SWATCH_BORDER,
color: theme.extended_palette().background.strong.color,
width: 1.0,
radius: 1.0.into(),
},
@@ -976,29 +916,27 @@ impl Ribbon {
crate::ui::icons::layer_lock(ll),
Message::LayerToggleLock(index),
);
let checkmark: Element<'_, Message> = container(if is_active {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let checkmark: Element<'_, Message> =
crate::ui::icons::themed_check_cell(is_active);
let label =
text(&info.name)
.size(11)
.color(if is_active { LABEL_ON } else { LABEL_OFF });
.style(move |theme: &Theme| iced::widget::text::Style {
color: (!is_active).then_some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
});
// The swatch + label area selects the layer as active; the
// icon buttons above handle their own toggles.
let select = button(row![swatch, label].spacing(5).align_y(iced::Center))
.on_press(Message::RibbonLayerChanged(name))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([4, 4]);
@@ -1021,34 +959,7 @@ impl Ribbon {
let search = iced::widget::text_input("Search layers…", &self.layer_filter)
.on_input(Message::RibbonLayerFilterChanged)
.size(11)
.padding([4, 6])
.style(|_: &Theme, _| iced::widget::text_input::Style {
background: Background::Color(Color {
r: 0.10,
g: 0.10,
b: 0.10,
a: 1.0,
}),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 2.0.into(),
},
icon: Color::WHITE,
placeholder: Color {
r: 0.45,
g: 0.45,
b: 0.45,
a: 1.0,
},
value: Color::WHITE,
selection: Color {
r: 0.20,
g: 0.44,
b: 0.72,
a: 0.5,
},
});
.padding([4, 6]);
let panel = container(
column![
container(search).padding([4, 4]),
@@ -1056,15 +967,7 @@ impl Ribbon {
]
.spacing(2),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(220.0));
let (align_right, h_pad, top) = self.dd_anchor(LAYER_COMBO_ID, 220.0, win.0);
@@ -1113,35 +1016,29 @@ impl Ribbon {
};
let active = ctx.active_for(style_key).to_string();
let row_style = |_: &Theme, status: button::Status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
};
let mut rows: Vec<Element<Message>> = ctx
.names_for(style_key)
.iter()
.map(|name| {
let is_sel = name.as_str() == active.as_str();
let n = name.clone();
let checkmark: Element<Message> = container(if is_sel {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let checkmark: Element<Message> =
crate::ui::icons::themed_check_cell(is_sel);
button(
row![
checkmark,
text(name.clone()).size(11).color(if is_sel {
LABEL_ON
} else {
LABEL_OFF
}),
text(name.clone())
.size(11)
.style(move |theme: &Theme| iced::widget::text::Style {
color: (!is_sel).then_some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
}),
]
.spacing(4)
.align_y(iced::Center),
@@ -1150,7 +1047,7 @@ impl Ribbon {
key: style_key,
name: n,
})
.style(row_style)
.style(popup_row_style)
.width(Fill)
.padding([4, 10])
.into()
@@ -1159,9 +1056,9 @@ impl Ribbon {
if let Some(mgr) = manager_cmd {
rows.push(
button(text("Manage…").size(11).color(LABEL_ON))
button(text("Manage…").size(11))
.on_press(Message::Command(mgr.to_string()))
.style(row_style)
.style(popup_row_style)
.width(Fill)
.padding([4, 10])
.into(),
@@ -1169,15 +1066,7 @@ impl Ribbon {
}
let panel = container(column(rows))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(LARGE_W * 2.3));
let (align_right, h_pad, top) = self.dd_anchor(open_id, LARGE_W * 2.3, win.0);
@@ -1200,15 +1089,7 @@ impl Ribbon {
);
let panel = container(picker)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(200.0));
let (align_right, h_pad, top) = self.dd_anchor(PROP_COLOR_ID, 200.0, win.0);
@@ -1243,23 +1124,22 @@ impl Ribbon {
.into_iter()
.map(|lt| {
let is_cur = lt.name == *active_lt;
let check: Element<'_, Message> = container(if is_cur {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let check: Element<'_, Message> =
crate::ui::icons::themed_check_cell(is_cur);
let name_col = text(lt.name.clone())
.size(11)
.color(if is_cur { LABEL_ON } else { LABEL_OFF })
.style(move |theme: &Theme| iced::widget::text::Style {
color: (!is_cur).then_some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
})
.width(Length::Fixed(90.0));
let art_col = text(lt.art.clone()).size(9).color(Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
});
let art_col = text(lt.art.clone()).size(9).style(muted_text_style);
let name = lt.name.clone();
button(
row![check, name_col, art_col]
@@ -1267,13 +1147,7 @@ impl Ribbon {
.align_y(iced::Center),
)
.on_press(Message::RibbonLinetypeChanged(name))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([4, 6])
.into()
@@ -1281,15 +1155,7 @@ impl Ribbon {
.collect();
let list = container(scrollable(column(rows)).height(Length::Fixed(200.0)))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(220.0));
let (align_right, h_pad, top) = self.dd_anchor(PROP_LINETYPE_ID, 220.0, win.0);
@@ -1308,31 +1174,29 @@ impl Ribbon {
.map(|item| {
let is_cur = item.0 == active_lw;
let label = item.to_string();
let check: Element<'_, Message> = container(if is_cur {
crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR)
} else {
iced::widget::Space::new().width(0).into()
})
.width(Length::Fixed(14.0))
.into();
let check: Element<'_, Message> =
crate::ui::icons::themed_check_cell(is_cur);
button(
row![
check,
text(label)
.size(11)
.color(if is_cur { LABEL_ON } else { LABEL_OFF })
.style(move |theme: &Theme| iced::widget::text::Style {
color: (!is_cur).then_some(
theme
.extended_palette()
.background
.base
.text
.scale_alpha(0.72),
),
})
]
.spacing(5)
.align_y(iced::Center),
)
.on_press(Message::RibbonLineweightChanged(item.0))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
_ => Color::TRANSPARENT,
})),
..Default::default()
})
.style(popup_row_style)
.width(Fill)
.padding([4, 8])
.into()
@@ -1350,15 +1214,7 @@ impl Ribbon {
win: (f32, f32),
) -> Option<Element<'a, Message>> {
let panel = container(column(rows))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.style(popup_panel_style)
.width(Length::Fixed(width));
let (align_right, h_pad, top) = self.dd_anchor(dd_id, width, win.0);
@@ -1439,7 +1295,7 @@ fn render_group<'a>(
column![
tools_el,
container(text(group.title).size(9).color(GROUP_LABEL)).padding([1, 4]),
container(text(group.title).size(9).style(muted_text_style)).padding([1, 4]),
]
.align_x(iced::Center)
.spacing(0)
@@ -1526,8 +1382,8 @@ fn collapse_button<'a>(
column![
icon,
row![
text(title.to_string()).size(9).color(GROUP_LABEL),
crate::ui::icons::arrow_down(8.0, GROUP_LABEL),
text(title.to_string()).size(9).style(muted_text_style),
crate::ui::icons::themed_secondary_arrow_down(8.0),
]
.spacing(3)
.align_y(iced::Center),
@@ -1536,22 +1392,7 @@ fn collapse_button<'a>(
.spacing(2),
)
.on_press(Message::ToggleRibbonPanel(title.to_string()))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered => Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
},
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 2.0.into(),
..Default::default()
},
..Default::default()
})
.style(button::subtle)
.padding([3, 5])
.into();
}
@@ -1594,29 +1435,14 @@ fn collapse_button<'a>(
let opener = button(
row![
text(title.to_string()).size(9).color(GROUP_LABEL),
crate::ui::icons::arrow_down(8.0, GROUP_LABEL),
text(title.to_string()).size(9).style(muted_text_style),
crate::ui::icons::themed_secondary_arrow_down(8.0),
]
.spacing(3)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonPanel(title.to_string()))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered => Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
},
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 2.0.into(),
..Default::default()
},
..Default::default()
})
.style(button::subtle)
.padding([1, 4]);
// The large face fills a fixed slot so a collapsed panel is shorter than a full
+197 -350
View File
@@ -8,7 +8,7 @@ use acadrust::types::{Color as AcadColor, LineWeight};
// Ribbon tooltips anchor to the right of their button so the cursor — which
// rests on the button itself — never covers the tip text. (#143)
use iced::widget::tooltip::Position as TipPos;
use iced::widget::{button, column, container, row, svg, text, tooltip};
use iced::widget::{button, column, container, row, text, tooltip};
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
use crate::app::Message;
@@ -77,175 +77,6 @@ pub(super) const PROP_COLOR_ID: &str = "PROP_COLOR";
pub(super) const PROP_LINETYPE_ID: &str = "PROP_LINETYPE";
pub(super) const PROP_LW_ID: &str = "PROP_LW";
// ── Colours ────────────────────────────────────────────────────────────────
/// Light chrome grey for the quick-access file-command icons on the top strip.
pub(super) const QA_ICON_COLOR: Color = Color {
r: 0.82,
g: 0.83,
b: 0.85,
a: 1.0,
};
pub(super) const TOPBAR_BG: Color = Color {
r: 0.17,
g: 0.17,
b: 0.17,
a: 1.0,
};
pub(super) const RIBBON_BG: Color = Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
};
pub(super) const BORDER_DARK: Color = Color {
r: 0.12,
g: 0.12,
b: 0.12,
a: 1.0,
};
pub(super) const ACCENT_BLUE: Color = Color {
r: 0.20,
g: 0.55,
b: 0.90,
a: 1.0,
};
pub(super) const ACCENT_GOLD: Color = Color {
r: 0.90,
g: 0.65,
b: 0.10,
a: 1.0,
};
pub(super) const LABEL_COLOR: Color = Color {
r: 0.82,
g: 0.82,
b: 0.82,
a: 1.0,
};
pub(super) const GROUP_LABEL: Color = Color {
r: 0.50,
g: 0.50,
b: 0.50,
a: 1.0,
};
pub(super) const TOOL_HOVER: Color = Color {
r: 0.32,
g: 0.32,
b: 0.32,
a: 1.0,
};
pub(super) const TOOL_ACTIVE: Color = Color {
r: 0.18,
g: 0.42,
b: 0.70,
a: 1.0,
};
pub(super) const ARROW_COLOR: Color = Color {
r: 0.65,
g: 0.65,
b: 0.65,
a: 1.0,
};
pub(super) const PANEL_BG: Color = Color {
r: 0.16,
g: 0.16,
b: 0.16,
a: 0.98,
};
pub(super) const PANEL_BORDER: Color = Color {
r: 0.32,
g: 0.32,
b: 0.32,
a: 1.0,
};
pub(super) const ROW_HOVER: Color = Color {
r: 0.24,
g: 0.24,
b: 0.24,
a: 1.0,
};
pub(super) const CHECK_COLOR: Color = Color {
r: 0.20,
g: 0.75,
b: 0.35,
a: 1.0,
};
pub(super) const ICON_COLOR: Color = Color {
r: 0.25,
g: 0.75,
b: 0.45,
a: 1.0,
};
pub(super) const LABEL_ON: Color = Color {
r: 0.92,
g: 0.92,
b: 0.92,
a: 1.0,
};
pub(super) const LABEL_OFF: Color = Color {
r: 0.72,
g: 0.72,
b: 0.72,
a: 1.0,
};
// ── Combo / dropdown colors ───────────────────────────────────────────────
pub(super) const COMBO_BG: Color = Color {
r: 0.18,
g: 0.18,
b: 0.18,
a: 1.0,
};
pub(super) const COMBO_HOVER_BG: Color = Color {
r: 0.26,
g: 0.26,
b: 0.26,
a: 1.0,
};
pub(super) const COMBO_OPEN_BG: Color = Color {
r: 0.14,
g: 0.14,
b: 0.14,
a: 1.0,
};
pub(super) const COMBO_BORDER: Color = Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
};
pub(super) const COMBO_ACTIVE_BORDER: Color = Color {
r: 0.45,
g: 0.65,
b: 0.90,
a: 1.0,
};
pub(super) const COMBO_ARROW: Color = Color {
r: 0.70,
g: 0.70,
b: 0.70,
a: 1.0,
};
pub(super) const SWATCH_BORDER: Color = Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.5,
};
pub(super) const TIP_BG: Color = Color {
r: 0.13,
g: 0.13,
b: 0.13,
a: 0.97,
};
pub(super) const HIST_INACTIVE_BG: Color = Color {
r: 0.20,
g: 0.20,
b: 0.20,
a: 1.0,
};
// ── Style context (passed from Ribbon to render_large) ────────────────────
pub(super) struct StyleContext {
@@ -294,11 +125,8 @@ pub(super) fn flush_small_col<'a>(
pub(super) fn make_icon(icon: IconKind, size: f32) -> Element<'static, Message> {
match icon {
IconKind::Glyph(s) => text(s).size(size * 0.7).color(Color::WHITE).into(),
IconKind::Svg(bytes) => {
let handle = svg::Handle::from_memory(bytes);
svg(handle).width(size).height(size).into()
}
IconKind::Glyph(s) => text(s).size(size * 0.7).into(),
IconKind::Svg(bytes) => icons::themed(bytes, size),
}
}
@@ -309,22 +137,19 @@ pub(super) fn start_dimmed(state: &ToggleState, event: &ModuleEvent) -> bool {
&& !matches!(event, ModuleEvent::Command(c) if crate::app::commands::start_allowed(c))
}
/// Label / glyph color for a possibly-dimmed tool.
pub(super) const DIM_TOOL: Color = Color {
r: 0.42,
g: 0.42,
b: 0.45,
a: 1.0,
};
/// `make_icon`, greyed out when `dim` (SVGs render monochrome via tint).
pub(super) fn make_icon_dim(icon: IconKind, size: f32, dim: bool) -> Element<'static, Message> {
if !dim {
return make_icon(icon, size);
}
match icon {
IconKind::Glyph(s) => text(s).size(size * 0.7).color(DIM_TOOL).into(),
IconKind::Svg(bytes) => icons::tinted(bytes, size, DIM_TOOL),
IconKind::Glyph(s) => text(s)
.size(size * 0.7)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.42)),
})
.into(),
IconKind::Svg(bytes) => icons::themed_disabled(bytes, size),
}
}
@@ -349,15 +174,24 @@ pub(super) fn is_active_tool(
// ── Button style ───────────────────────────────────────────────────────────
pub(super) fn tool_btn_style(is_active: bool, status: button::Status) -> button::Style {
pub(super) fn tool_btn_style(
theme: &Theme,
is_active: bool,
status: button::Status,
) -> button::Style {
let palette = theme.extended_palette();
let pair = match (is_active, status) {
(true, _) => palette.primary.weak,
(_, button::Status::Hovered) => palette.background.weak,
(_, button::Status::Pressed) => palette.primary.weak,
_ => palette.background.base,
};
button::Style {
background: Some(Background::Color(match (is_active, status) {
(true, _) => TOOL_ACTIVE,
(_, button::Status::Hovered) => TOOL_HOVER,
(_, button::Status::Pressed) => TOOL_ACTIVE,
_ => Color::TRANSPARENT,
})),
text_color: Color::WHITE,
background: is_active
.then_some(Background::Color(pair.color))
.or_else(|| matches!(status, button::Status::Hovered | button::Status::Pressed)
.then_some(Background::Color(pair.color))),
text_color: pair.text,
border: Border {
radius: 3.0.into(),
color: Color::TRANSPARENT,
@@ -368,21 +202,93 @@ pub(super) fn tool_btn_style(is_active: bool, status: button::Status) -> button:
}
}
// ── Tooltip helpers ────────────────────────────────────────────────────────
pub(super) fn make_tip(tip: String) -> Element<'static, Message> {
text(tip).size(11).color(Color::WHITE).into()
pub(super) fn combo_btn_style(
theme: &Theme,
is_open: bool,
status: button::Status,
radius: f32,
) -> button::Style {
let palette = theme.extended_palette();
let pair = if is_open {
palette.primary.weak
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
palette.background.weak
} else {
palette.background.weakest
};
button::Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
border: Border {
radius: radius.into(),
width: 1.0,
color: if is_open {
palette.primary.base.color
} else {
palette.background.neutral.color
},
},
..Default::default()
}
}
pub(super) fn tip_style(_theme: &Theme) -> container::Style {
pub(super) fn popup_row_style(theme: &Theme, status: button::Status) -> button::Style {
let palette = theme.extended_palette();
let pair = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
palette.background.weak
} else {
palette.background.base
};
button::Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
..Default::default()
}
}
pub(super) fn popup_panel_style(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(TIP_BG)),
background: Some(Background::Color(palette.background.base.color)),
border: Border {
color: COMBO_BORDER,
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
text_color: Some(Color::WHITE),
..Default::default()
}
}
pub(super) fn muted_text_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.72)),
}
}
pub(super) fn tool_label_style(theme: &Theme, dim: bool) -> iced::widget::text::Style {
iced::widget::text::Style {
color: dim.then_some(
theme.extended_palette().background.base.text.scale_alpha(0.42),
),
}
}
// ── Tooltip helpers ────────────────────────────────────────────────────────
pub(super) fn make_tip(tip: String) -> Element<'static, Message> {
text(tip).size(11).into()
}
pub(super) fn tip_style(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.strong.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
text_color: Some(palette.background.strong.text),
..Default::default()
}
}
@@ -408,7 +314,7 @@ pub(super) fn render_small<'a>(
let tip_text = format!("{}\nCommand: {}", t.label, t.id);
let btn = button(make_icon_dim(t.icon, SMALL_ICON, dim))
.on_press(Message::RibbonToolClick { tool_id, event })
.style(move |_: &Theme, status| tool_btn_style(active, status))
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(SMALL_W))
.height(ROW_H)
.padding([4, 4]);
@@ -474,31 +380,22 @@ pub(super) fn render_small<'a>(
tool_id: last.to_string(),
event: ModuleEvent::Command(last.to_string()),
})
.style(move |_: &Theme, status| tool_btn_style(active, status))
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(SMALL_W))
.height(ROW_H)
.padding([4, 4]);
let arr_tip = format!("{} options", cur_label);
let arr_btn = button(
container(icons::arrow_down(8.0, ARROW_COLOR))
container(icons::themed_arrow_down(8.0))
.width(Fill)
.height(Fill)
.align_x(iced::Center)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(id.to_string()))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => TOOL_HOVER,
_ if dd_open => TOOL_ACTIVE,
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 2.0.into(),
..Default::default()
},
..Default::default()
.style(move |theme: &Theme, status| {
tool_btn_style(theme, dd_open, status)
})
.width(Length::Fixed(ARROW_W))
.height(ROW_H)
@@ -569,7 +466,9 @@ pub(super) fn render_large_dropdown<'a>(
let top_btn = button(
column![
make_icon_dim(cur_icon, LARGE_ICON, dim),
text(label.to_string()).size(10).color(if dim { DIM_TOOL } else { LABEL_COLOR }),
text(label.to_string())
.size(10)
.style(move |theme: &Theme| tool_label_style(theme, dim)),
]
.align_x(iced::Center)
.spacing(3),
@@ -578,7 +477,7 @@ pub(super) fn render_large_dropdown<'a>(
tool_id: last.to_string(),
event: ModuleEvent::Command(last.to_string()),
})
.style(move |_: &Theme, status| tool_btn_style(active, status))
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(LARGE_W))
.height(Fill)
.padding(Padding {
@@ -589,24 +488,15 @@ pub(super) fn render_large_dropdown<'a>(
});
let arr_btn = button(
container(icons::arrow_down(9.0, ARROW_COLOR))
container(icons::themed_arrow_down(9.0))
.width(Fill)
.height(Fill)
.align_x(iced::Center)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(id.to_string()))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => TOOL_HOVER,
_ if dd_open => TOOL_ACTIVE,
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 3.0.into(),
..Default::default()
},
..Default::default()
.style(move |theme: &Theme, status| {
tool_btn_style(theme, dd_open, status)
})
.width(Length::Fixed(LARGE_W))
.height(LARGE_ARR)
@@ -659,13 +549,15 @@ pub(super) fn render_large<'a>(
let btn = button(
column![
make_icon_dim(t.icon, LARGE_ICON, dim),
text(t.label).size(10).color(if dim { DIM_TOOL } else { LABEL_COLOR }),
text(t.label)
.size(10)
.style(move |theme: &Theme| tool_label_style(theme, dim)),
]
.align_x(iced::Center)
.spacing(3),
)
.on_press(Message::RibbonToolClick { tool_id, event })
.style(move |_: &Theme, status| tool_btn_style(active, status))
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
.width(Length::Fixed(LARGE_W))
.height(Fill)
.padding(Padding {
@@ -737,10 +629,10 @@ pub(super) fn render_large<'a>(
let freeze_icon = icons::raw(icons::layer_freeze(lf), 14.0);
let lock_icon = icons::raw(icons::layer_lock(ll), 14.0);
let swatch = container(text(""))
.style(move |_: &Theme| container::Style {
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(lc)),
border: Border {
color: SWATCH_BORDER,
color: theme.extended_palette().background.strong.color,
width: 1.0,
radius: 1.0.into(),
},
@@ -758,27 +650,17 @@ pub(super) fn render_large<'a>(
freeze_icon,
lock_icon,
swatch,
container(text(active_layer).size(11).color(Color::WHITE))
container(text(active_layer).size(11))
.width(name_w)
.clip(true),
icons::arrow_down(9.0, COMBO_ARROW),
icons::themed_arrow_down(9.0),
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(LAYER_COMBO_ID.to_string()))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match (is_open, status) {
(true, _) => COMBO_OPEN_BG,
(_, button::Status::Hovered) => COMBO_HOVER_BG,
_ => COMBO_BG,
})),
border: Border {
radius: 3.0.into(),
width: 1.0,
color: COMBO_BORDER,
},
..Default::default()
.style(move |theme: &Theme, status| {
combo_btn_style(theme, is_open, status, 3.0)
})
.padding([3, 8])
.width(Fill);
@@ -793,20 +675,16 @@ pub(super) fn render_large<'a>(
let event = t.event.clone();
let icon_el: Element<Message> = if dim {
make_icon_dim(t.icon, 16.0, true)
} else { match t.icon {
IconKind::Glyph(g) => text(g).size(13).color(Color::WHITE).into(),
IconKind::Svg(bytes) => {
iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes))
.width(16)
.height(16)
.into()
}
} };
} else {
make_icon(t.icon, 16.0)
};
let msg = module_event_to_message(event);
tooltip(
button(icon_el)
.on_press(msg)
.style(move |_: &Theme, status| tool_btn_style(is_active, status))
.style(move |theme: &Theme, status| {
tool_btn_style(theme, is_active, status)
})
.padding([2, 5]),
make_tip(tip.to_string()),
TipPos::Right,
@@ -864,7 +742,9 @@ pub(super) fn render_large<'a>(
let mp_btn = button(
column![
make_icon_dim(match_prop.icon, LARGE_ICON, mp_dim),
text(match_prop.label).size(10).color(if mp_dim { DIM_TOOL } else { LABEL_COLOR }),
text(match_prop.label)
.size(10)
.style(move |theme: &Theme| tool_label_style(theme, mp_dim)),
]
.align_x(iced::Center)
.spacing(3),
@@ -873,7 +753,7 @@ pub(super) fn render_large<'a>(
tool_id: mp_id,
event: mp_event,
})
.style(move |_: &Theme, status| tool_btn_style(mp_active, status))
.style(move |theme: &Theme, status| tool_btn_style(theme, mp_active, status))
.width(Length::Fixed(LARGE_W))
.height(Fill)
.padding(Padding {
@@ -895,10 +775,10 @@ pub(super) fn render_large<'a>(
let is_open = open_dd.as_deref() == Some(dd_id);
let swatch_el: Element<'a, Message> = if let Some(c) = swatch {
container(text(""))
.style(move |_: &Theme| container::Style {
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(c)),
border: Border {
color: SWATCH_BORDER,
color: theme.extended_palette().background.strong.color,
width: 1.0,
radius: 1.0.into(),
},
@@ -913,40 +793,21 @@ pub(super) fn render_large<'a>(
button(
row![
swatch_el,
container(text(label).size(10).color(Color::WHITE))
container(text(label).size(10))
.width(Fill)
.clip(true),
icons::arrow_toggle(
is_open,
8.0,
Color {
r: 0.6,
g: 0.6,
b: 0.6,
a: 1.0,
},
),
if is_open {
icons::themed_arrow_up(8.0)
} else {
icons::themed_arrow_down(8.0)
},
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(dd_id.to_string()))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match (is_open, status) {
(true, _) => COMBO_OPEN_BG,
(_, button::Status::Hovered) => COMBO_HOVER_BG,
_ => COMBO_BG,
})),
border: Border {
radius: 2.0.into(),
width: 1.0,
color: if is_open {
COMBO_ACTIVE_BORDER
} else {
COMBO_BORDER
},
},
..Default::default()
.style(move |theme: &Theme, status| {
combo_btn_style(theme, is_open, status, 2.0)
})
.padding([3, 8])
.width(Length::Fixed(PROP_W))
@@ -999,31 +860,21 @@ pub(super) fn render_large<'a>(
// ── combo button ──
let combo_btn = button(
row![
container(text(active.clone()).size(11).color(Color::WHITE))
container(text(active.clone()).size(11))
.width(Fill)
.clip(true),
icons::arrow_toggle(is_open, 9.0, COMBO_ARROW),
if is_open {
icons::themed_arrow_up(9.0)
} else {
icons::themed_arrow_down(9.0)
},
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(combo_id.to_string()))
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match (is_open, status) {
(true, _) => COMBO_OPEN_BG,
(_, button::Status::Hovered) => COMBO_HOVER_BG,
_ => COMBO_BG,
})),
border: Border {
radius: 3.0.into(),
width: 1.0,
color: if is_open {
COMBO_ACTIVE_BORDER
} else {
COMBO_BORDER
},
},
..Default::default()
.style(move |theme: &Theme, status| {
combo_btn_style(theme, is_open, status, 3.0)
})
.padding([3, 8])
.width(Fill);
@@ -1045,20 +896,16 @@ pub(super) fn render_large<'a>(
let event = t.event.clone();
let icon_el: Element<Message> = if dim {
make_icon_dim(t.icon, 16.0, true)
} else { match t.icon {
IconKind::Glyph(g) => text(g).size(13).color(Color::WHITE).into(),
IconKind::Svg(bytes) => {
iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes))
.width(16)
.height(16)
.into()
}
} };
} else {
make_icon(t.icon, 16.0)
};
let msg = module_event_to_message(event);
tooltip(
button(icon_el)
.on_press(msg)
.style(move |_: &Theme, status| tool_btn_style(is_active, status))
.style(move |theme: &Theme, status| {
tool_btn_style(theme, is_active, status)
})
.padding([2, 5]),
make_tip(tip.to_string()),
TipPos::Right,
@@ -1128,7 +975,11 @@ pub(super) fn quick_access_btn<'a>(
// they read on the dark top strip (raw black is invisible there).
// On the Start tab, commands the start gate refuses render dimmed.
let dim = is_start && !crate::app::commands::start_allowed(cmd);
let icon = icons::tinted(icon_bytes, 16.0, if dim { DIM_TOOL } else { QA_ICON_COLOR });
let icon = if dim {
icons::themed_disabled(icon_bytes, 16.0)
} else {
icons::themed(icon_bytes, 16.0)
};
let btn = button(
container(icon)
.width(Fill)
@@ -1137,22 +988,7 @@ pub(super) fn quick_access_btn<'a>(
.align_y(iced::Center),
)
.on_press(Message::Command(cmd.to_string()))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => Color {
r: 0.30,
g: 0.30,
b: 0.30,
a: 1.0,
},
_ => Color::TRANSPARENT,
})),
border: Border {
radius: 2.0.into(),
..Default::default()
},
..Default::default()
})
.style(button::subtle)
.width(Length::Fixed(TOP_HIST_W))
.height(24)
.padding([2, 0]);
@@ -1171,13 +1007,12 @@ pub(super) fn render_history_control<'a>(
) -> Element<'a, Message> {
let dd_open = open_dropdown.as_deref() == Some(dropdown_id);
let active = count > 0;
let icon_color = if active { Color::WHITE } else { LABEL_OFF };
let main_btn = {
let glyph = if dropdown_id == UNDO_HISTORY_ID {
icons::undo(15.0, icon_color)
icons::themed_undo(15.0, active)
} else {
icons::redo(15.0, icon_color)
icons::themed_redo(15.0, active)
};
let btn = button(
container(glyph)
@@ -1186,7 +1021,9 @@ pub(super) fn render_history_control<'a>(
.align_x(iced::Center)
.align_y(iced::Center),
)
.style(move |_: &Theme, status| top_hist_btn_style(active, dd_open, status))
.style(move |theme: &Theme, status| {
top_hist_btn_style(theme, active, dd_open, status)
})
.width(Length::Fixed(TOP_HIST_W))
.height(24)
.padding([2, 0]);
@@ -1211,16 +1048,19 @@ pub(super) fn render_history_control<'a>(
let arrow_btn = {
let btn = button(
container(icons::arrow_down(
8.0,
if active { ARROW_COLOR } else { LABEL_OFF },
))
container(if active {
icons::themed_arrow_down(8.0)
} else {
icons::themed_disabled_arrow_down(8.0)
})
.width(Fill)
.height(Fill)
.align_x(iced::Center)
.align_y(iced::Center),
)
.style(move |_: &Theme, status| top_hist_btn_style(active, dd_open, status))
.style(move |theme: &Theme, status| {
top_hist_btn_style(theme, active, dd_open, status)
})
.width(Length::Fixed(TOP_ARR_W))
.height(24)
.padding(0);
@@ -1243,19 +1083,26 @@ pub(super) fn render_history_control<'a>(
}
pub(super) fn top_hist_btn_style(
theme: &Theme,
active: bool,
open: bool,
status: button::Status,
) -> button::Style {
let palette = theme.extended_palette();
let pair = match (active, open, status) {
(false, _, _) => palette.background.weakest,
(_, true, _) => palette.primary.weak,
(_, _, button::Status::Hovered) => palette.background.weak,
(_, _, button::Status::Pressed) => palette.primary.weak,
_ => palette.background.base,
};
button::Style {
background: Some(Background::Color(match (active, open, status) {
(false, _, _) => HIST_INACTIVE_BG,
(_, true, _) => TOOL_ACTIVE,
(_, _, button::Status::Hovered) => TOOL_HOVER,
(_, _, button::Status::Pressed) => TOOL_ACTIVE,
_ => Color::TRANSPARENT,
})),
text_color: Color::WHITE,
background: (!active || open || matches!(
status,
button::Status::Hovered | button::Status::Pressed
))
.then_some(Background::Color(pair.color)),
text_color: pair.text,
border: Border {
radius: 3.0.into(),
color: Color::TRANSPARENT,