mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8537f1c658 |
@@ -57,7 +57,7 @@
|
||||
"error": "#fff0ca",
|
||||
"field": "#100b04",
|
||||
"form_accent": "#ca892b",
|
||||
"form_dim": "#9b6922",
|
||||
"form_dim": "#ad7a2c",
|
||||
"form_error": "#fff0ca",
|
||||
"form_error_fill": "#46300f",
|
||||
"form_fill": "#e19e45",
|
||||
@@ -180,7 +180,7 @@
|
||||
"surface_soft": "#2a1e0b",
|
||||
"tab_on_fill_dark": "#170d03",
|
||||
"text": "#f3ca87",
|
||||
"text_dim": "#9b6922",
|
||||
"text_dim": "#ad7a2c",
|
||||
"timeline": "#090602",
|
||||
"timeline_item": "#1d1406",
|
||||
"timeline_item_error": "#46300f",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"form_dim": "#bdae93",
|
||||
"form_primary": "#83a598",
|
||||
"form_error": "#fb4934",
|
||||
"form_error_fill": "#702b28",
|
||||
"form_error_fill": "#5f2320",
|
||||
"form_success": "#b8bb26",
|
||||
"form_success_fill": "#343a28",
|
||||
"dialog_body": "#32302f",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"form_accent": "#cfa7c7",
|
||||
"form_dim": "#a4b0c2",
|
||||
"form_error": "#bf616a",
|
||||
"form_error_fill": "#4b3d48",
|
||||
"form_error_fill": "#352b33",
|
||||
"form_fill": "#e7bfe0",
|
||||
"form_ink": "#eceff4",
|
||||
"form_primary": "#88c0d0",
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
"surface_soft": "#363332",
|
||||
"tab_on_fill_dark": "#000000",
|
||||
"text": "#abb2bf",
|
||||
"text_dim": "#7f848e",
|
||||
"text_dim": "#90959f",
|
||||
"timeline": "#1e2227",
|
||||
"timeline_item": "#323842",
|
||||
"timeline_item_error": "#5c343d",
|
||||
|
||||
@@ -59,12 +59,12 @@
|
||||
"form_accent": "#6b70c3",
|
||||
"form_dim": "#657b83",
|
||||
"form_error": "#dc322f",
|
||||
"form_error_fill": "#eac4b4",
|
||||
"form_error_fill": "#f7ddd3",
|
||||
"form_fill": "#a2a9f8",
|
||||
"form_ink": "#586e75",
|
||||
"form_primary": "#268bd2",
|
||||
"form_soft": "#e4ded4",
|
||||
"form_success": "#859900",
|
||||
"form_success": "#5f6e00",
|
||||
"form_success_fill": "#dcdbb1",
|
||||
"form_tab_ink": "#565a9d",
|
||||
"glyph_bg": "#e3ddc9",
|
||||
@@ -130,6 +130,8 @@
|
||||
"mesh_fill": "#d7aa3c",
|
||||
"mesh_ink": "#7b5d00",
|
||||
"mesh_soft": "#e9e0c4",
|
||||
"nav_tip": "#073642",
|
||||
"nav_tip_ink": "#eee8d5",
|
||||
"on_accent": "#ffffff",
|
||||
"origin_plane": "#b5890028",
|
||||
"origin_plane_edge": "#b5890088",
|
||||
@@ -179,7 +181,7 @@
|
||||
"surface_ink": "#a83e12",
|
||||
"surface_soft": "#ebdbc6",
|
||||
"tab_on_fill_dark": "#fdf6e3",
|
||||
"text": "#586e75",
|
||||
"text": "#536970",
|
||||
"text_dim": "#657b83",
|
||||
"timeline": "#e3ddc9",
|
||||
"timeline_item": "#e3ddc9",
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"form_ink": "#000000",
|
||||
"form_primary": "#000080",
|
||||
"form_soft": "#bbb1bb",
|
||||
"form_success": "#008000",
|
||||
"form_success": "#006000",
|
||||
"form_success_fill": "#c0dcc0",
|
||||
"form_tab_ink": "#7d007d",
|
||||
"glyph_bg": "#ffffff",
|
||||
@@ -130,6 +130,10 @@
|
||||
"mesh_fill": "#808000",
|
||||
"mesh_ink": "#4c4c00",
|
||||
"mesh_soft": "#bbbbb1",
|
||||
"nav_press": "#a0a0a0",
|
||||
"nav_tip": "#ffffe1",
|
||||
"nav_tip_ink": "#000000",
|
||||
"nav_tip_key_border": "#808080",
|
||||
"on_accent": "#ffffff",
|
||||
"origin_plane": "#ffff005e",
|
||||
"origin_plane_edge": "#808000c8",
|
||||
@@ -180,7 +184,7 @@
|
||||
"surface_soft": "#bbb6b1",
|
||||
"tab_on_fill_dark": "#000000",
|
||||
"text": "#000000",
|
||||
"text_dim": "#808080",
|
||||
"text_dim": "#606060",
|
||||
"timeline": "#c0c0c0",
|
||||
"timeline_item": "#c0c0c0",
|
||||
"timeline_item_error": "#ffffc0",
|
||||
|
||||
@@ -297,7 +297,7 @@ fn paint_icon(ui: &egui::Ui, rect: egui::Rect, tex: Option<egui::TextureId>, nam
|
||||
egui::Align2::CENTER_CENTER,
|
||||
letter,
|
||||
egui::FontId::proportional(crate::theme::Tokens::get().font_small),
|
||||
t.text,
|
||||
crate::theme::readable(&[t.chip], crate::theme::TEXT_CONTRAST, &[t.text, t.on_accent]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +286,28 @@ pub(crate) fn contrast_ratio(a: Color32, b: Color32) -> f64 {
|
||||
(a.max(b) + 0.05) / (a.min(b) + 0.05)
|
||||
}
|
||||
|
||||
/// WCAG floors: body text, and icons, bold labels and other large marks.
|
||||
pub(crate) const TEXT_CONTRAST: f64 = 4.5;
|
||||
pub(crate) const MARK_CONTRAST: f64 = 3.0;
|
||||
|
||||
/// The first of `inks` that reads at `min` on every one of `fills`, else whichever of them reads
|
||||
/// best on its worst fill. Themes older than a surface can pair a fill with an ink that vanishes
|
||||
/// on it (SolidCraft 95's navy accent on its navy selection); this keeps such a pair readable
|
||||
/// without changing any theme where the preferred ink already reads.
|
||||
pub(crate) fn readable(fills: &[Color32], min: f64, inks: &[Color32]) -> Color32 {
|
||||
let worst = |ink: Color32| fills.iter().map(|fill| contrast_ratio(ink, *fill)).fold(f64::INFINITY, f64::min);
|
||||
inks.iter()
|
||||
.copied()
|
||||
.find(|ink| worst(*ink) >= min)
|
||||
.or_else(|| inks.iter().copied().max_by(|a, b| worst(*a).total_cmp(&worst(*b))))
|
||||
.unwrap_or(Color32::BLACK)
|
||||
}
|
||||
|
||||
/// Is `fill` dark enough that light artwork reads better on it?
|
||||
pub(crate) fn is_dark_fill(fill: Color32) -> bool {
|
||||
contrast_ratio(Color32::WHITE, fill) > contrast_ratio(Color32::BLACK, fill)
|
||||
}
|
||||
|
||||
impl TabAccent {
|
||||
pub fn for_tab(t: &Tokens, tab: &str) -> Self {
|
||||
let (color, ink, soft) = match tab {
|
||||
@@ -872,6 +894,17 @@ impl Tokens {
|
||||
egui::Shadow { offset: [0, self.space_2 as i8], blur: self.space_8 as u8, spread: 0, color: self.app_bar.gamma_multiply(0.18) }
|
||||
}
|
||||
|
||||
/// The accent ink (`form_primary`) on `fill`, or the theme's selected or body ink where the
|
||||
/// accent does not read there: a theme whose selection or highlight is its accent colour.
|
||||
pub(crate) fn accent_on(&self, fill: Color32) -> Color32 {
|
||||
readable(&[fill], MARK_CONTRAST, &[self.form_primary, self.selected_ink, self.form_ink])
|
||||
}
|
||||
|
||||
/// `ink` on the selected fill, or `selected_ink` where `ink` does not read at `min` there.
|
||||
pub(crate) fn on_selected(&self, ink: Color32, min: f64) -> Color32 {
|
||||
readable(&[self.selected_bg], min, &[ink, self.selected_ink])
|
||||
}
|
||||
|
||||
/// The X, Y and Z axis colours, in that order.
|
||||
pub const fn axes(&self) -> [Color32; 3] {
|
||||
[self.axis_x, self.axis_y, self.axis_z]
|
||||
|
||||
@@ -509,10 +509,38 @@ pub fn resolve(theme: &ThemeFile) -> Tokens {
|
||||
// A file that sets no colours is its base palette, exactly.
|
||||
if !theme.colors.is_empty() {
|
||||
file_view_from_theme(&mut t, theme.base.is_dark(), |k| theme.colors.contains_key(k));
|
||||
navigation_from_theme(&mut t, |k| theme.colors.contains_key(k));
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
/// The navigation bar and its menus are newer than most theme files: the navigation colours a
|
||||
/// theme leaves unset follow its own hover, dim text, control and dialog colours instead of its
|
||||
/// base's blue-greys, and its tooltip is the theme's ink and dialog colours turned around.
|
||||
fn navigation_from_theme(t: &mut Tokens, set: impl Fn(&str) -> bool) {
|
||||
if !set("nav_hover") {
|
||||
t.nav_hover = t.hover;
|
||||
}
|
||||
if !set("nav_press") {
|
||||
t.nav_press = t.nav_hover.lerp_to_gamma(t.form_ink, 0.12);
|
||||
}
|
||||
if !set("nav_dim") {
|
||||
t.nav_dim = t.text_dim;
|
||||
}
|
||||
if !set("nav_field") {
|
||||
t.nav_field = t.control_fill;
|
||||
}
|
||||
if !set("nav_viewport") {
|
||||
t.nav_viewport = t.dialog_body;
|
||||
}
|
||||
if !set("nav_tip") {
|
||||
t.nav_tip = t.form_ink;
|
||||
}
|
||||
if !set("nav_tip_ink") {
|
||||
t.nav_tip_ink = t.dialog_body;
|
||||
}
|
||||
}
|
||||
|
||||
/// The File view's colours a theme leaves unset follow the theme's own panel, border, accent and
|
||||
/// text, not its base's fixed blue rail and grey cards (#4868). Each ink is whichever candidate
|
||||
/// reads best on the fills it sits on.
|
||||
|
||||
@@ -290,7 +290,13 @@ pub(crate) fn offer_pill(
|
||||
let glyph = Rect::from_center_size(pos2(glyph_x, rect.center().y), vec2(16.0, 16.0));
|
||||
icons::paint_on(ui.painter(), glyph, icon, Some(true), 1.0);
|
||||
if let Some((text, _)) = label {
|
||||
ui.painter().text(pos2(rect.left() + text_x, rect.center().y), Align2::LEFT_CENTER, text, font, t.app_bar_text);
|
||||
// The pill is the theme's accent: a light accent (the phosphor themes) needs dark ink.
|
||||
let ink = if hot {
|
||||
t.app_bar_text
|
||||
} else {
|
||||
crate::theme::readable(&[fill], crate::theme::MARK_CONTRAST, &[t.app_bar_text, t.on_accent, t.tab_on_fill_dark])
|
||||
};
|
||||
ui.painter().text(pos2(rect.left() + text_x, rect.center().y), Align2::LEFT_CENTER, text, font, ink);
|
||||
}
|
||||
Some((rect, response))
|
||||
}
|
||||
|
||||
@@ -269,6 +269,8 @@ fn action_button(ui: &mut Ui, label: &str, kind: Action, look: &ActionLook, padd
|
||||
look.primary_ink
|
||||
} else if primary || !live {
|
||||
t.form_dim
|
||||
} else if marked {
|
||||
t.on_selected(t.form_ink, crate::theme::TEXT_CONTRAST)
|
||||
} else {
|
||||
t.form_ink
|
||||
});
|
||||
@@ -403,22 +405,26 @@ pub(crate) fn button_pair(ui: &mut egui::Ui, key: &str, states: [bool; 2], butto
|
||||
egui::StrokeKind::Inside,
|
||||
);
|
||||
let label = crate::i18n::tr_label(button.label);
|
||||
let galley = ui.painter().layout_no_wrap(label.to_string(), egui::FontId::proportional(t.font_label), t.form_ink);
|
||||
let ink = if on { t.on_selected(t.form_ink, crate::theme::TEXT_CONTRAST) } else { t.form_ink };
|
||||
let galley = ui.painter().layout_no_wrap(label.to_string(), egui::FontId::proportional(t.font_label), ink);
|
||||
let icon_width = if button.icon.is_some() { t.sketch_footer_icon_size + t.space_4 } else { 0.0 };
|
||||
let left = rect.center().x - (galley.size().x + icon_width) / 2.0;
|
||||
let painter = ui.painter().with_clip_rect(ui.clip_rect().intersect(rect));
|
||||
if let Some(icon) = button.icon {
|
||||
crate::icons::paint_icon(
|
||||
// A selection darker or lighter than the theme takes that side's artwork.
|
||||
let side = (on && crate::theme::is_dark_fill(t.selected_bg) != crate::theme::is_dark()).then_some(!crate::theme::is_dark());
|
||||
crate::icons::paint_on(
|
||||
&painter,
|
||||
egui::Rect::from_center_size(
|
||||
egui::pos2(left + t.sketch_footer_icon_size / 2.0, rect.center().y),
|
||||
vec2(t.sketch_footer_icon_size, t.sketch_footer_icon_size),
|
||||
),
|
||||
icon,
|
||||
side,
|
||||
f32::from(t.icon.a()) / 255.0,
|
||||
);
|
||||
}
|
||||
painter.galley(egui::pos2(left + icon_width, rect.center().y - galley.size().y / 2.0), galley, t.form_ink);
|
||||
painter.galley(egui::pos2(left + icon_width, rect.center().y - galley.size().y / 2.0), galley, ink);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), on, label));
|
||||
t.button_bevel(ui, &response, on);
|
||||
pointer_on_hover(ui, &response);
|
||||
@@ -602,7 +608,7 @@ pub(crate) fn compact_button(ui: &mut Ui, glyph: &str, tooltip: &str, enabled: b
|
||||
let ink = if !enabled {
|
||||
t.form_dim.gamma_multiply(0.35)
|
||||
} else if selected {
|
||||
t.form_primary
|
||||
t.accent_on(t.selected_bg)
|
||||
} else {
|
||||
t.form_dim
|
||||
};
|
||||
|
||||
@@ -164,7 +164,18 @@ pub(crate) fn segmented(ui: &mut Ui, selected: &mut usize, options: &[&str], con
|
||||
egui::pos2(text_rect.left() - t.space_6 - t.space_14 / 2.0, segment.center().y),
|
||||
vec2(t.space_14, t.space_14),
|
||||
);
|
||||
crate::icons::paint_ink(ui.painter(), icon_rect, icon, if on && workspace_accent { accent.ink } else { t.form_ink });
|
||||
crate::icons::paint_ink(
|
||||
ui.painter(),
|
||||
icon_rect,
|
||||
icon,
|
||||
if on && workspace_accent {
|
||||
accent.ink
|
||||
} else if on && !primary {
|
||||
t.on_selected(t.form_ink, crate::theme::MARK_CONTRAST)
|
||||
} else {
|
||||
t.form_ink
|
||||
},
|
||||
);
|
||||
}
|
||||
let text_rect = if compact { text_rect.intersect(segment.shrink2(vec2(t.space_4, 0.0))) } else { text_rect };
|
||||
paint_text(
|
||||
@@ -262,6 +273,10 @@ pub(crate) fn segmented_icons(ui: &mut Ui, selected: &mut usize, options: &[Icon
|
||||
if catalog {
|
||||
let icon_rect = egui::Rect::from_center_size(segment.center(), vec2(t.space_14, t.space_14));
|
||||
crate::icons::paint_ink(ui.painter(), icon_rect, option.icon, if on { accent.ink } else { t.form_dim });
|
||||
} else if on && crate::theme::is_dark_fill(t.selected_bg) != crate::theme::is_dark() {
|
||||
// A selection darker or lighter than the theme (SolidCraft 95's navy) takes the
|
||||
// artwork drawn for that side, so the option's line or dash still shows.
|
||||
crate::icons::paint_on(ui.painter(), icon_rect, option.icon, Some(!crate::theme::is_dark()), f32::from(t.icon.a()) / 255.0);
|
||||
} else {
|
||||
crate::icons::paint_icon(ui.painter(), icon_rect, option.icon, f32::from(t.icon.a()) / 255.0);
|
||||
}
|
||||
@@ -662,8 +677,16 @@ pub(crate) fn popup_list(
|
||||
inner.min,
|
||||
egui::pos2((inner.right() - kind_width - t.space_8).max(inner.left()), inner.bottom()),
|
||||
);
|
||||
super::text::paint_text(ui, text_rect, option.text, mono.clone(), if picked { t.form_primary } else { option.color }, false);
|
||||
ui.painter().text(inner.right_center(), egui::Align2::RIGHT_CENTER, kind, kind_font.clone(), t.form_dim);
|
||||
super::text::paint_text(
|
||||
ui,
|
||||
text_rect,
|
||||
option.text,
|
||||
mono.clone(),
|
||||
if picked { t.accent_on(t.selected_bg) } else { option.color },
|
||||
false,
|
||||
);
|
||||
let kind_ink = if picked { t.on_selected(t.form_dim, crate::theme::MARK_CONTRAST) } else { t.form_dim };
|
||||
ui.painter().text(inner.right_center(), egui::Align2::RIGHT_CENTER, kind, kind_font.clone(), kind_ink);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, ui.is_enabled(), picked, option.text));
|
||||
// Tip the full name first: long identifiers still clip inside a capped popup.
|
||||
let tip = if option.detail.is_empty() { option.text.to_owned() } else { format!("{}\n{}", option.text, option.detail) };
|
||||
|
||||
@@ -56,7 +56,13 @@ pub(crate) fn count_field(ui: &mut Ui, value: &mut String, limits: &Count<'_>) -
|
||||
if enabled && arrow.hovered() {
|
||||
painter.rect_filled(half.shrink(1.0), t.control_radius, t.selected_bg);
|
||||
}
|
||||
let ink = if enabled { t.form_ink } else { t.form_dim };
|
||||
let ink = if !enabled {
|
||||
t.form_dim
|
||||
} else if arrow.hovered() {
|
||||
t.on_selected(t.form_ink, crate::theme::MARK_CONTRAST)
|
||||
} else {
|
||||
t.form_ink
|
||||
};
|
||||
let (c, w, h) = (half.center(), (reserve * 0.18).max(2.0), (half.height() * 0.2).max(2.0));
|
||||
let tip = if delta > 0 { -h } else { h };
|
||||
painter.add(egui::Shape::convex_polygon(
|
||||
|
||||
@@ -277,14 +277,14 @@ impl<'a> MenuItem<'a> {
|
||||
let ink = if self.danger {
|
||||
t.form_error
|
||||
} else if hot {
|
||||
t.form_primary
|
||||
t.accent_on(t.accent_soft)
|
||||
} else {
|
||||
t.form_ink
|
||||
};
|
||||
let dim = if self.danger {
|
||||
t.form_error
|
||||
} else if hot {
|
||||
t.form_primary
|
||||
t.accent_on(t.accent_soft)
|
||||
} else {
|
||||
t.form_dim
|
||||
};
|
||||
@@ -299,7 +299,7 @@ impl<'a> MenuItem<'a> {
|
||||
let icon = Rect::from_center_size(pos2(r.left() + t.control_height / 2.0, r.center().y), vec2(t.space_16, t.space_16));
|
||||
if let Some(on) = self.checked {
|
||||
if on {
|
||||
super::checks::paint_check_glyph(&painter, icon, false, t.form_primary);
|
||||
super::checks::paint_check_glyph(&painter, icon, false, if hot { ink } else { t.form_primary });
|
||||
}
|
||||
} else if let Some(color) = self.swatch {
|
||||
painter.rect(icon, t.radius_sm, color, t.control_border_stroke(t.control_border), egui::StrokeKind::Inside);
|
||||
@@ -419,7 +419,7 @@ pub(crate) fn workspace_menu(ui: &mut egui::Ui, items: &[Item], key_row: Option<
|
||||
let badge = ui.painter().layout_no_wrap(
|
||||
badge.to_string(),
|
||||
FontId::proportional(crate::theme::Tokens::get().font_small),
|
||||
if current { t.on_accent } else { accent },
|
||||
if current { crate::theme::readable(&[accent], crate::theme::MARK_CONTRAST, &[t.on_accent, t.tab_on_fill_dark]) } else { accent },
|
||||
);
|
||||
let badge_rect = Rect::from_min_size(pos2(badge_x, y - 8.0), badge.size() + vec2(t.space_12, t.space_3));
|
||||
ui.painter().rect_filled(badge_rect, t.radius_lg, if current { accent } else { soft });
|
||||
|
||||
@@ -104,11 +104,16 @@ pub(crate) fn navigation_fill(t: &Tokens, fill: NavigationFill) -> Option<egui::
|
||||
/// The label and icon colour of a slot. An active slot keeps the accent ink on its selected fill
|
||||
/// where that reads; a theme whose selected fill is as dark as its accent (SolidCraft 95's navy)
|
||||
/// gets the theme's selected-text ink instead, so an active toggle never turns into a blank block.
|
||||
/// A hovered or pressed slot keeps the body ink unless the theme's press colour is as dark as
|
||||
/// it (1-bit's black press), where the selected-text ink takes over too.
|
||||
pub(crate) fn navigation_ink(t: &Tokens, fill: NavigationFill) -> egui::Color32 {
|
||||
if fill != NavigationFill::Active {
|
||||
return t.form_ink;
|
||||
let readable = |fill, ink| crate::theme::readable(&[fill], NAV_ACTIVE_MIN_CONTRAST, &[ink, t.selected_ink]);
|
||||
match fill {
|
||||
NavigationFill::None => t.form_ink,
|
||||
NavigationFill::Hover => readable(t.nav_hover, t.form_ink),
|
||||
NavigationFill::Press => readable(t.nav_press, t.form_ink),
|
||||
NavigationFill::Active => readable(t.selected_bg, t.form_primary),
|
||||
}
|
||||
if crate::theme::contrast_ratio(t.form_primary, t.selected_bg) >= NAV_ACTIVE_MIN_CONTRAST { t.form_primary } else { t.selected_ink }
|
||||
}
|
||||
|
||||
/// WCAG small-text contrast for an active slot's label on its selected fill.
|
||||
@@ -246,4 +251,88 @@ mod active_ink_tests {
|
||||
}
|
||||
assert!(failures.is_empty(), "active navigation slot unreadable:\n{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
/// Every fill the newer UI paints text or an icon on, with the ink it paints there, reads in
|
||||
/// every built-in theme: the navigation bar and its menus, viewport status items, the
|
||||
/// ribbon's drop-downs and workspace menu, the Render stages, toggles and segmented options,
|
||||
/// badges and the sketch status drawn on the view. A theme older than one of these surfaces
|
||||
/// left it with its base palette's colour, or with its accent on an accent-coloured fill
|
||||
/// (SolidCraft 95's navy on navy).
|
||||
#[test]
|
||||
fn newer_ui_text_and_icons_stay_readable_in_every_built_in_theme() {
|
||||
use crate::theme::{MARK_CONTRAST, TEXT_CONTRAST, TabAccent, Tokens, contrast_ratio, readable};
|
||||
use egui::Color32;
|
||||
type Pick = fn(&Tokens) -> Color32;
|
||||
let status: Pick = |t| readable(&[t.viewport_top, t.viewport_bottom], MARK_CONTRAST, &[t.sketch_accent, t.sketch_ink, t.text]);
|
||||
let conflict: Pick = |t| readable(&[t.viewport_top, t.viewport_bottom], MARK_CONTRAST, &[t.error, t.form_error, t.text]);
|
||||
// (where, fill, ink, floor)
|
||||
let pairs: &[(&str, Pick, Pick, f64)] = &[
|
||||
("selected label: navigation menu, Preferences pages, picked rows", |t| t.selected_bg, |t| t.accent_on(t.selected_bg), MARK_CONTRAST),
|
||||
("navigation menu: selected row detail", |t| t.selected_bg, |t| t.on_selected(t.form_dim, MARK_CONTRAST), MARK_CONTRAST),
|
||||
("navigation bar: hovered slot", |t| t.nav_hover, |t| navigation_ink(t, NavigationFill::Hover), TEXT_CONTRAST),
|
||||
("navigation bar: pressed slot", |t| t.nav_press, |t| navigation_ink(t, NavigationFill::Press), TEXT_CONTRAST),
|
||||
("navigation menu: hovered row icon", |t| t.nav_hover, |t| t.form_dim, MARK_CONTRAST),
|
||||
("navigation settings: add slot", |t| t.nav_hover, |t| t.accent_on(t.nav_hover), MARK_CONTRAST),
|
||||
("navigation tooltip", |t| t.nav_tip, |t| t.nav_tip_ink, TEXT_CONTRAST),
|
||||
("viewport status item: name", |t| t.overlay, |t| t.text, TEXT_CONTRAST),
|
||||
("viewport status item: setting", |t| t.overlay, |t| t.text_dim, MARK_CONTRAST),
|
||||
("ribbon: section label", |t| t.toolbar, |t| t.text_dim, MARK_CONTRAST),
|
||||
("ribbon: hovered section label", |t| t.hover, |t| t.text_dim, MARK_CONTRAST),
|
||||
("panels: secondary text", |t| t.panel, |t| t.text_dim, MARK_CONTRAST),
|
||||
("ribbon drop-down: highlighted row", |t| t.accent_soft, |t| t.accent_on(t.accent_soft), MARK_CONTRAST),
|
||||
(
|
||||
"workspace menu: current badge",
|
||||
|t| t.accent,
|
||||
|t| readable(&[t.accent], MARK_CONTRAST, &[t.on_accent, t.tab_on_fill_dark]),
|
||||
MARK_CONTRAST,
|
||||
),
|
||||
(
|
||||
"Render stage bar: current stage number",
|
||||
|t| TabAccent::for_tab(t, "RENDER").color,
|
||||
|t| TabAccent::for_tab(t, "RENDER").on_fill(t),
|
||||
MARK_CONTRAST,
|
||||
),
|
||||
("toggle buttons, keyboard-marked action: on", |t| t.selected_bg, |t| t.on_selected(t.form_ink, TEXT_CONTRAST), TEXT_CONTRAST),
|
||||
("toggle and segmented icons: on", |t| t.selected_bg, |t| t.on_selected(t.form_ink, MARK_CONTRAST), MARK_CONTRAST),
|
||||
("segmented choice: selected label", |t| t.selected_bg, |t| t.selected_ink, TEXT_CONTRAST),
|
||||
("settings and search badges: valid", |t| t.form_success_fill, |t| t.form_success, MARK_CONTRAST),
|
||||
("search badges: invalid", |t| t.form_error_fill, |t| t.form_error, MARK_CONTRAST),
|
||||
("dialogs: success text", |t| t.dialog_body, |t| t.form_success, MARK_CONTRAST),
|
||||
(
|
||||
"title bar: offer pill",
|
||||
|t| t.accent,
|
||||
|t| readable(&[t.accent], MARK_CONTRAST, &[t.app_bar_text, t.on_accent, t.tab_on_fill_dark]),
|
||||
MARK_CONTRAST,
|
||||
),
|
||||
("save mesh: lettered tile", |t| t.chip, |t| readable(&[t.chip], TEXT_CONTRAST, &[t.text, t.on_accent]), MARK_CONTRAST),
|
||||
("sketch status on the view, top", |t| t.viewport_top, status, MARK_CONTRAST),
|
||||
("sketch status on the view, bottom", |t| t.viewport_bottom, status, MARK_CONTRAST),
|
||||
("sketch conflict status on the view, top", |t| t.viewport_top, conflict, MARK_CONTRAST),
|
||||
("sketch conflict status on the view, bottom", |t| t.viewport_bottom, conflict, MARK_CONTRAST),
|
||||
];
|
||||
// A translucent fill is seen over the panel it floats on, translucent ink over its fill.
|
||||
let solid = |c: Color32, under: Color32| {
|
||||
if c.a() == 255 {
|
||||
return c;
|
||||
}
|
||||
let k = f32::from(c.a()) / 255.0;
|
||||
let [r, g, b, _] = c.to_srgba_unmultiplied();
|
||||
let mix = |x: u8, y: u8| (f32::from(x) * k + f32::from(y) * (1.0 - k)).round() as u8;
|
||||
Color32::from_rgb(mix(r, under.r()), mix(g, under.g()), mix(b, under.b()))
|
||||
};
|
||||
let mut themes = vec![("Dark".to_owned(), Tokens::dark()), ("Light".to_owned(), Tokens::light())];
|
||||
themes.extend(crate::theme_files::builtin().iter().map(|theme| (theme.name.clone(), crate::theme_files::resolve(theme))));
|
||||
let mut failures = Vec::new();
|
||||
for (name, t) in &themes {
|
||||
for (place, fill, ink, floor) in pairs {
|
||||
let fill = solid(fill(t), t.panel);
|
||||
let ink = solid(ink(t), fill);
|
||||
let ratio = contrast_ratio(ink, fill);
|
||||
if ratio < *floor {
|
||||
failures.push(format!("{name}: {place}: {ratio:.2}:1 ({ink:?} on {fill:?})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "unreadable in a built-in theme:\n{}", failures.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ impl<'a> NavigationMenuRow<'a> {
|
||||
let t = Tokens::get();
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width().max(0.0), NAV_MENU_ROW), Sense::hover());
|
||||
let response = ui.interact(rect, ui.id().with(self.id), if self.enabled { Sense::click() } else { Sense::hover() });
|
||||
let ink = if self.selected { t.form_primary } else { t.form_ink };
|
||||
let ink = if self.selected { t.accent_on(t.selected_bg) } else { t.form_ink };
|
||||
let bg = if self.selected {
|
||||
Some(t.selected_bg)
|
||||
} else if response.hovered() && self.enabled {
|
||||
@@ -69,7 +69,13 @@ impl<'a> NavigationMenuRow<'a> {
|
||||
Align2::RIGHT_CENTER,
|
||||
detail,
|
||||
FontId::proportional(if self.enabled { t.font_body } else { t.font_small }),
|
||||
if self.enabled { t.form_dim } else { t.nav_dim },
|
||||
if !self.enabled {
|
||||
t.nav_dim
|
||||
} else if self.selected {
|
||||
t.on_selected(t.form_dim, crate::theme::MARK_CONTRAST)
|
||||
} else {
|
||||
t.form_dim
|
||||
},
|
||||
);
|
||||
end = end.min(detail.left() - t.space_8);
|
||||
}
|
||||
@@ -81,7 +87,7 @@ impl<'a> NavigationMenuRow<'a> {
|
||||
ui.painter().galley(pos2(left, rect.center().y - galley.size().y / 2.0), galley, ink);
|
||||
let tail = Rect::from_center_size(pos2(rect.right() - NAV_MENU_INSET - t.space_14 / 2.0, rect.center().y), vec2(t.space_14, t.space_14));
|
||||
if self.selected {
|
||||
super::checks::paint_check_glyph(ui.painter(), tail, false, t.form_primary);
|
||||
super::checks::paint_check_glyph(ui.painter(), tail, false, ink);
|
||||
} else if self.submenu {
|
||||
let arrow =
|
||||
Rect::from_center_size(pos2(rect.right() - NAV_MENU_INSET - NAV_CHEVRON / 2.0, rect.center().y), vec2(NAV_CHEVRON, NAV_CHEVRON));
|
||||
|
||||
@@ -63,7 +63,7 @@ pub(crate) fn navigation_tile(ui: &mut Ui, id: &str, label: &str, icon: &str, dr
|
||||
if add.hovered() {
|
||||
painter.rect_filled(add_rect, t.control_radius.saturating_sub(NAV_BORDER as u8), t.nav_hover);
|
||||
}
|
||||
icons::paint_ink(&painter, Rect::from_center_size(add_rect.center(), vec2(t.space_14, t.space_14)), "plus", t.form_primary);
|
||||
icons::paint_ink(&painter, Rect::from_center_size(add_rect.center(), vec2(t.space_14, t.space_14)), "plus", t.accent_on(t.nav_hover));
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), label));
|
||||
add.widget_info(|| {
|
||||
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), crate::i18n::format("navbar.custom.add_slot", &[("slot", label)]))
|
||||
|
||||
@@ -1234,7 +1234,7 @@ fn page_list_sized<S>(
|
||||
if on || response.hovered() || response.has_focus() {
|
||||
ui.painter().rect_filled(rect, t.control_radius, if on { t.selected_bg } else { t.hover });
|
||||
}
|
||||
let ink = if on { t.form_primary } else { t.form_ink };
|
||||
let ink = if on { t.accent_on(t.selected_bg) } else { t.form_ink };
|
||||
let icon =
|
||||
egui::Rect::from_center_size(egui::pos2(rect.left() + t.space_10 + t.space_8, rect.center().y), vec2(t.space_16, t.space_16));
|
||||
crate::icons::paint_ink(ui.painter(), icon, page.icon, if on { ink } else { t.form_dim });
|
||||
|
||||
@@ -186,7 +186,7 @@ impl StageBar {
|
||||
},
|
||||
&(index + 1).to_string(),
|
||||
crate::fonts::semibold(t.font_small),
|
||||
if on { t.on_accent } else { t.text_dim },
|
||||
if on { accent.on_fill(&t) } else { t.text_dim },
|
||||
false,
|
||||
);
|
||||
let text = Rect::from_min_max(pos2(badge.right() + t.space_6 + 1.0, chip.top()), chip.right_bottom() - vec2(t.space_14, 0.0));
|
||||
|
||||
@@ -352,7 +352,7 @@ pub(crate) fn plus_header<R>(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> Opt
|
||||
let response = super::actions::icon_button_colored(ui, rect, "plus", "Add Column", Some(t.form_dim));
|
||||
if response.hovered() {
|
||||
ui.painter().rect_filled(rect, t.control_radius, t.selected_bg);
|
||||
crate::icons::paint_ink(ui.painter(), rect.shrink(t.form_icon_inset), "plus", t.form_primary);
|
||||
crate::icons::paint_ink(ui.painter(), rect.shrink(t.form_icon_inset), "plus", t.accent_on(t.selected_bg));
|
||||
for x in [rect.left() - t.form_gap_y, rect.right() + t.form_gap_y] {
|
||||
ui.painter().vline(x, rect.y_range(), egui::Stroke::new(t.space_2, t.form_primary));
|
||||
}
|
||||
|
||||
@@ -5949,7 +5949,10 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
crate::i18n::format("sketch.degrees_of_freedom", &[("count", &ss.report.dof.to_string())])
|
||||
};
|
||||
let msg = format!("{} — {status}", crate::i18n::tr_name(&ss.name));
|
||||
let c = if ss.report.ok() { v.sketch_accent } else { v.error };
|
||||
// Straight on the view's gradient: a theme whose sketch or error colour is close to its
|
||||
// background (SolidCraft 95's green and red on teal) gets the view's text colour.
|
||||
let inks = if ss.report.ok() { [v.sketch_accent, v.sketch_ink, v.text] } else { [v.error, v.form_error, v.text] };
|
||||
let c = crate::theme::readable(&[v.viewport_top, v.viewport_bottom], crate::theme::MARK_CONTRAST, &inks);
|
||||
painter.text(pos2(r.left() + 12.0, r.top() + 12.0), Align2::LEFT_TOP, msg, FontId::proportional(t.font_button), c);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user