forked from 1b-agent/opencadstudio-upstream
Add the Fusion Black and Fusion White themes
Fusion Black pairs near-black chrome with a paper-white canvas, so the drawing is the only bright thing on screen. That breaks the assumption in theme_canvas_background that the canvas follows the chrome, hence the explicit lookup: falling through to the palette would paint model space black and undo the point of the theme. Both keep a blue accent rather than going fully greyscale. Selection highlight defaults to the theme primary, and a white primary would be invisible against a white canvas. These are Theme::Custom values and so are absent from iced's Theme::ALL. all_themes() is the list to enumerate from now; the picker in the options dialog uses it, and parse_theme_name resolves both by name.
This commit is contained in:
4 files changed
+163
-4
No files matched your search
+21
-3
@@ -176,11 +176,20 @@ impl UiThemePalette {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn builtin_theme(name: &str) -> Option<iced::Theme> {
|
||||
/// Every theme the user may pick: iced's built-ins followed by the Fusion
|
||||
/// pair. The Fusion themes are `Theme::Custom`, so they are not in
|
||||
/// `iced::Theme::ALL`; anything enumerating themes for display or for test
|
||||
/// coverage must use this instead, or they silently vanish from the list.
|
||||
pub fn all_themes() -> Vec<iced::Theme> {
|
||||
iced::Theme::ALL
|
||||
.iter()
|
||||
.find(|theme| theme.to_string() == name)
|
||||
.cloned()
|
||||
.chain(crate::ui::style::fusion_theme::fusion_themes())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn builtin_theme(name: &str) -> Option<iced::Theme> {
|
||||
all_themes().into_iter().find(|theme| theme.to_string() == name)
|
||||
}
|
||||
|
||||
fn color_to_rgb(color: iced::Color) -> [u8; 3] {
|
||||
@@ -376,7 +385,14 @@ pub fn theme_canvas_background(theme: &iced::Theme) -> [u8; 3] {
|
||||
iced::Theme::Nightfly => [1, 22, 39],
|
||||
iced::Theme::Oxocarbon => [22, 22, 22],
|
||||
iced::Theme::Ferra => [43, 41, 46],
|
||||
_ => color_to_rgb(theme.palette().background.base.color),
|
||||
// The Fusion pair deliberately breaks the "canvas follows chrome"
|
||||
// rule: black chrome is paired with a white canvas, so falling
|
||||
// through to the palette below would paint model space black and
|
||||
// undo the whole point of the theme.
|
||||
other => match crate::ui::style::fusion_theme::fusion_canvas(other) {
|
||||
Some(rgb) => rgb,
|
||||
None => color_to_rgb(other.palette().background.base.color),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +423,8 @@ pub fn parse_theme_name(s: &str) -> Option<iced::Theme> {
|
||||
"NIGHTFLY" => Some(iced::Theme::Nightfly),
|
||||
"OXOCARBON" => Some(iced::Theme::Oxocarbon),
|
||||
"FERRA" => Some(iced::Theme::Ferra),
|
||||
"FUSIONBLACK" => Some(crate::ui::style::fusion_theme::fusion_black()),
|
||||
"FUSIONWHITE" => Some(crate::ui::style::fusion_theme::fusion_white()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
//! The Fusion theme pair: black chrome around a white canvas, and its
|
||||
//! inverse.
|
||||
//!
|
||||
//! "Mixed black/white" is about the split, not about draining every colour
|
||||
//! out of the UI. Ribbon, docks, status bar and command line sit at
|
||||
//! near-black while model space stays paper-white, so the drawing is the
|
||||
//! only bright thing on screen. One accent survives that split: selection
|
||||
//! highlight defaults to the theme primary
|
||||
//! (`ModelSpaceThemeConfig::selection_highlight_color` = 0), and a white
|
||||
//! primary would be invisible against a white canvas. The accent is
|
||||
//! therefore a saturated blue that reads against both halves.
|
||||
//!
|
||||
//! These are `iced::Theme::Custom` values, so they are absent from
|
||||
//! `iced::Theme::ALL`. Anything enumerating themes must go through
|
||||
//! [`crate::app::config::all_themes`] instead.
|
||||
|
||||
use iced::theme::palette::Seed;
|
||||
use iced::{Color, Theme};
|
||||
|
||||
/// Display name of the black-chrome theme. Persisted verbatim in
|
||||
/// `settings.json` as `theme.name`, so changing it orphans a user's choice.
|
||||
pub const FUSION_BLACK: &str = "Fusion Black";
|
||||
|
||||
/// Display name of the light-chrome theme.
|
||||
pub const FUSION_WHITE: &str = "Fusion White";
|
||||
|
||||
/// Model-space canvas for [`FUSION_BLACK`] — paper white against the
|
||||
/// near-black chrome. Deliberately not pure #FFFFFF: a fraction below white
|
||||
/// keeps the paper-space sheet distinguishable from the model canvas.
|
||||
pub const FUSION_BLACK_CANVAS: [u8; 3] = [250, 250, 250];
|
||||
|
||||
/// Model-space canvas for [`FUSION_WHITE`].
|
||||
pub const FUSION_WHITE_CANVAS: [u8; 3] = [255, 255, 255];
|
||||
|
||||
fn rgb(hex: u32) -> Color {
|
||||
Color::from_rgb8(
|
||||
((hex >> 16) & 0xFF) as u8,
|
||||
((hex >> 8) & 0xFF) as u8,
|
||||
(hex & 0xFF) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
/// Near-black chrome, white text, blue accent.
|
||||
pub fn fusion_black() -> Theme {
|
||||
Theme::custom(
|
||||
FUSION_BLACK,
|
||||
Seed {
|
||||
background: rgb(0x1A1A1A),
|
||||
text: rgb(0xF2F2F2),
|
||||
primary: rgb(0x0696D7),
|
||||
success: rgb(0x4CAF50),
|
||||
warning: rgb(0xFFB300),
|
||||
danger: rgb(0xE53935),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// The inverse: light chrome, near-black text. The accent darkens so it
|
||||
/// keeps its contrast against a light surface.
|
||||
pub fn fusion_white() -> Theme {
|
||||
Theme::custom(
|
||||
FUSION_WHITE,
|
||||
Seed {
|
||||
background: rgb(0xFAFAFA),
|
||||
text: rgb(0x1A1A1A),
|
||||
primary: rgb(0x0277BD),
|
||||
success: rgb(0x2E7D32),
|
||||
warning: rgb(0xE65100),
|
||||
danger: rgb(0xC62828),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Both Fusion themes, in the order the theme picker should show them.
|
||||
pub fn fusion_themes() -> Vec<Theme> {
|
||||
vec![fusion_black(), fusion_white()]
|
||||
}
|
||||
|
||||
/// The canvas colour a Fusion theme asks for, or `None` for any other theme.
|
||||
/// Keyed by name because `Theme::Custom` carries no discriminant to match on.
|
||||
pub fn fusion_canvas(theme: &Theme) -> Option<[u8; 3]> {
|
||||
match theme.to_string().as_str() {
|
||||
FUSION_BLACK => Some(FUSION_BLACK_CANVAS),
|
||||
FUSION_WHITE => Some(FUSION_WHITE_CANVAS),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ui::style::common::wcag_contrast;
|
||||
|
||||
/// The same bar `theme_accessibility_tests` holds the built-in themes to.
|
||||
/// These themes are hand-written rather than generated, so nothing else
|
||||
/// would catch a palette typo that drops text below AA.
|
||||
#[test]
|
||||
fn fusion_themes_meet_wcag_aa_on_every_surface() {
|
||||
for theme in fusion_themes() {
|
||||
let p = theme.palette();
|
||||
for (name, pair) in [
|
||||
("base", p.background.base),
|
||||
("weak", p.background.weak),
|
||||
("strong", p.background.strong),
|
||||
("weakest", p.background.weakest),
|
||||
] {
|
||||
let contrast = wcag_contrast(pair.text, pair.color);
|
||||
assert!(
|
||||
contrast >= 4.5,
|
||||
"{theme} {name} surface text is {contrast:.2}:1, below AA"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The accent exists so selection stays visible on the white canvas. If
|
||||
/// it ever drifts toward white this catches it.
|
||||
#[test]
|
||||
fn fusion_accent_is_visible_on_the_canvas() {
|
||||
for (theme, canvas) in [
|
||||
(fusion_black(), FUSION_BLACK_CANVAS),
|
||||
(fusion_white(), FUSION_WHITE_CANVAS),
|
||||
] {
|
||||
let canvas = Color::from_rgb8(canvas[0], canvas[1], canvas[2]);
|
||||
let contrast = wcag_contrast(theme.palette().primary.base.color, canvas);
|
||||
assert!(
|
||||
contrast >= 3.0,
|
||||
"{theme} accent is {contrast:.2}:1 on its canvas, too faint \
|
||||
to show a selection"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fusion_canvas_is_keyed_by_name() {
|
||||
assert_eq!(fusion_canvas(&fusion_black()), Some(FUSION_BLACK_CANVAS));
|
||||
assert_eq!(fusion_canvas(&fusion_white()), Some(FUSION_WHITE_CANVAS));
|
||||
assert_eq!(fusion_canvas(&Theme::Dark), None);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub mod anno_object_scale;
|
||||
pub mod style_manager;
|
||||
pub mod common;
|
||||
pub mod form;
|
||||
pub mod fusion_theme;
|
||||
|
||||
#[cfg(test)]
|
||||
mod common_tests {
|
||||
|
||||
@@ -179,7 +179,7 @@ pub fn view_window<'a>(
|
||||
.copied()
|
||||
.find(|candidate| *candidate == default_save_format);
|
||||
|
||||
let theme_options = Theme::ALL
|
||||
let theme_options = crate::app::config::all_themes()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.chain(std::iter::once("Custom".to_string()))
|
||||
|
||||
Reference in new issue
Block a user