Strengthen palette roles and source selection contrast
This commit is contained in:
@@ -61,13 +61,13 @@ impl Highlighter for ArcadiaHighlighter {
|
|||||||
|
|
||||||
pub fn format(class: &TokenClass, _theme: &Theme) -> Format<Font> {
|
pub fn format(class: &TokenClass, _theme: &Theme) -> Format<Font> {
|
||||||
let color = match class {
|
let color = match class {
|
||||||
TokenClass::Keyword => Color::from_rgb8(118, 62, 160),
|
TokenClass::Keyword => Color::from_rgb8(51, 23, 72),
|
||||||
TokenClass::Identifier => Color::from_rgb8(20, 93, 139),
|
TokenClass::Identifier => Color::from_rgb8(4, 39, 61),
|
||||||
TokenClass::String => Color::from_rgb8(24, 113, 79),
|
TokenClass::String => Color::from_rgb8(4, 42, 27),
|
||||||
TokenClass::Number => Color::from_rgb8(163, 78, 30),
|
TokenClass::Number => Color::from_rgb8(63, 26, 6),
|
||||||
TokenClass::Comment => Color::from_rgb8(111, 121, 132),
|
TokenClass::Comment => Color::from_rgb8(33, 37, 41),
|
||||||
TokenClass::Error => Color::from_rgb8(186, 38, 62),
|
TokenClass::Error => Color::from_rgb8(76, 9, 20),
|
||||||
TokenClass::Punctuation | TokenClass::Whitespace => Color::from_rgb8(91, 101, 117),
|
TokenClass::Punctuation | TokenClass::Whitespace => Color::from_rgb8(32, 37, 44),
|
||||||
};
|
};
|
||||||
Format {
|
Format {
|
||||||
color: Some(color),
|
color: Some(color),
|
||||||
|
|||||||
@@ -1,26 +1,75 @@
|
|||||||
use iced::{
|
use iced::{
|
||||||
Border, Color, Theme,
|
Border, Color, Theme,
|
||||||
widget::{button, container},
|
theme::palette::{Extended, Pair},
|
||||||
|
widget::{button, container, text_editor},
|
||||||
};
|
};
|
||||||
use oa_core::Kind;
|
use oa_core::Kind;
|
||||||
pub const BG: Color = Color::from_rgb(0.945, 0.956, 0.969);
|
pub const BG: Color = Color::from_rgb(0.945, 0.956, 0.969);
|
||||||
pub const INK: Color = Color::from_rgb(0.13, 0.18, 0.25);
|
pub const INK: Color = Color::from_rgb8(24, 33, 44);
|
||||||
pub const MUTED: Color = Color::from_rgb(0.43, 0.48, 0.55);
|
pub const MUTED: Color = Color::from_rgb8(75, 89, 104);
|
||||||
pub const TEAL: Color = Color::from_rgb(0.0, 0.48, 0.43);
|
pub const TEAL: Color = Color::from_rgb8(0, 102, 92);
|
||||||
|
/// Decorative dividers; essential outlines use BOUNDARY instead.
|
||||||
pub const LINE: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
pub const LINE: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||||
|
pub const BOUNDARY: Color = Color::from_rgb8(102, 117, 134);
|
||||||
|
const WARNING: Color = Color::from_rgb8(134, 91, 0);
|
||||||
|
const SELECTION: Color = Color::from_rgb8(217, 238, 232);
|
||||||
|
// Ordinary values remain INK; syntax selection has its own surface and palette.
|
||||||
|
const INPUT_SELECTION: Color = Color::from_rgb8(123, 142, 134);
|
||||||
|
const SOURCE_SELECTION: Color = Color::from_rgb8(128, 149, 139);
|
||||||
|
const SOURCE_INK: Color = Color::from_rgb8(33, 37, 41);
|
||||||
pub fn theme() -> Theme {
|
pub fn theme() -> Theme {
|
||||||
Theme::custom(
|
Theme::custom_with_fn(
|
||||||
"Arcadia Light",
|
"Arcadia Light",
|
||||||
iced::theme::Palette {
|
iced::theme::Palette {
|
||||||
background: BG,
|
background: BG,
|
||||||
text: INK,
|
text: INK,
|
||||||
primary: TEAL,
|
primary: TEAL,
|
||||||
success: TEAL,
|
success: TEAL,
|
||||||
warning: Color::from_rgb(0.72, 0.47, 0.10),
|
warning: WARNING,
|
||||||
danger: Color::from_rgb(0.78, 0.22, 0.24),
|
danger: Color::from_rgb(0.78, 0.22, 0.24),
|
||||||
},
|
},
|
||||||
|
|palette| {
|
||||||
|
let mut extended = Extended::generate(palette);
|
||||||
|
// Pair::new adjusts text toward a 6:1 target. Keep these reviewed
|
||||||
|
// colors exact; resolved style tests enforce each consumer contract.
|
||||||
|
let pair = |color, text| Pair { color, text };
|
||||||
|
extended.background.base = pair(BG, INK);
|
||||||
|
extended.background.weakest = pair(Color::WHITE, INK);
|
||||||
|
extended.background.weaker = pair(BG, INK);
|
||||||
|
// These pairs also supply pick-list/menu and disabled-control fills.
|
||||||
|
extended.background.weak = pair(Color::WHITE, INK);
|
||||||
|
extended.background.neutral = pair(LINE, INK);
|
||||||
|
extended.background.strong = pair(BOUNDARY, Color::WHITE);
|
||||||
|
extended.background.stronger = pair(MUTED, Color::WHITE);
|
||||||
|
extended.background.strongest = pair(MUTED, Color::WHITE);
|
||||||
|
extended.primary.base = pair(TEAL, Color::WHITE);
|
||||||
|
extended.primary.weak = pair(INPUT_SELECTION, INK);
|
||||||
|
extended.primary.strong = pair(Color::from_rgb8(0, 83, 75), Color::WHITE);
|
||||||
|
// Secondary base is both placeholder ink and a button background.
|
||||||
|
extended.secondary.base = pair(MUTED, Color::WHITE);
|
||||||
|
extended.secondary.weak = pair(LINE, INK);
|
||||||
|
extended.secondary.strong = pair(Color::from_rgb8(54, 68, 82), Color::WHITE);
|
||||||
|
extended.success.base = extended.primary.base;
|
||||||
|
extended.success.weak = pair(SELECTION, INK);
|
||||||
|
extended.success.strong = extended.primary.strong;
|
||||||
|
extended.warning.base = pair(WARNING, Color::WHITE);
|
||||||
|
extended.warning.weak = pair(Color::from_rgb8(250, 235, 203), INK);
|
||||||
|
extended.warning.strong = pair(Color::from_rgb8(103, 70, 0), Color::WHITE);
|
||||||
|
extended.danger.base = pair(palette.danger, Color::WHITE);
|
||||||
|
extended.danger.weak = pair(Color::from_rgb8(249, 227, 229), INK);
|
||||||
|
extended.danger.strong = pair(Color::from_rgb8(156, 34, 40), Color::WHITE);
|
||||||
|
extended
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
pub fn source_editor(theme: &Theme, status: text_editor::Status) -> text_editor::Style {
|
||||||
|
let mut style = text_editor::default(theme, status);
|
||||||
|
style.background = Color::WHITE.into();
|
||||||
|
style.value = SOURCE_INK;
|
||||||
|
style.placeholder = SOURCE_INK;
|
||||||
|
style.selection = SOURCE_SELECTION;
|
||||||
|
style
|
||||||
|
}
|
||||||
pub fn card(_: &Theme) -> container::Style {
|
pub fn card(_: &Theme) -> container::Style {
|
||||||
container::Style {
|
container::Style {
|
||||||
background: Some(Color::WHITE.into()),
|
background: Some(Color::WHITE.into()),
|
||||||
@@ -66,11 +115,10 @@ pub fn nav(theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
}
|
}
|
||||||
pub fn selected(theme: &Theme, status: button::Status) -> button::Style {
|
pub fn selected(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let mut s = nav(theme, status);
|
let mut s = nav(theme, status);
|
||||||
s.background = Some(Color::from_rgb8(218, 238, 235).into());
|
s.background = Some(SELECTION.into());
|
||||||
s.text_color = INK;
|
|
||||||
s.border = Border {
|
s.border = Border {
|
||||||
color: Color::from_rgb8(151, 203, 195),
|
color: TEAL,
|
||||||
width: 1.,
|
width: 2.,
|
||||||
radius: 7.into(),
|
radius: 7.into(),
|
||||||
};
|
};
|
||||||
s
|
s
|
||||||
@@ -78,10 +126,10 @@ pub fn selected(theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
pub fn kind_color(kind: Kind) -> Color {
|
pub fn kind_color(kind: Kind) -> Color {
|
||||||
match kind {
|
match kind {
|
||||||
Kind::Entity | Kind::System => Color::from_rgb(0.16, 0.40, 0.71),
|
Kind::Entity | Kind::System => Color::from_rgb(0.16, 0.40, 0.71),
|
||||||
Kind::Actor | Kind::SystemActor | Kind::Role => Color::from_rgb(0.58, 0.35, 0.70),
|
Kind::Actor | Kind::SystemActor | Kind::Role => Color::from_rgb8(117, 68, 153),
|
||||||
Kind::Activity | Kind::Function => TEAL,
|
Kind::Activity | Kind::Function => TEAL,
|
||||||
Kind::Capability | Kind::SystemCapability | Kind::Mission | Kind::OperationalMission => {
|
Kind::Capability | Kind::SystemCapability | Kind::Mission | Kind::OperationalMission => {
|
||||||
Color::from_rgb(0.72, 0.47, 0.10)
|
WARNING
|
||||||
}
|
}
|
||||||
Kind::Process
|
Kind::Process
|
||||||
| Kind::FunctionalChain
|
| Kind::FunctionalChain
|
||||||
@@ -94,6 +142,9 @@ pub fn kind_color(kind: Kind) -> Color {
|
|||||||
| Kind::Mode
|
| Kind::Mode
|
||||||
| Kind::Initial
|
| Kind::Initial
|
||||||
| Kind::Final => Color::from_rgb(0.38, 0.39, 0.68),
|
| Kind::Final => Color::from_rgb(0.38, 0.39, 0.68),
|
||||||
_ => Color::from_rgb(0.25, 0.48, 0.56),
|
_ => Color::from_rgb8(50, 107, 128),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
use super::*;
|
||||||
|
use iced::{
|
||||||
|
Background,
|
||||||
|
widget::{checkbox, overlay::menu, pick_list, text_input},
|
||||||
|
};
|
||||||
|
use oa_core::language::TokenClass;
|
||||||
|
|
||||||
|
fn luminance(color: Color) -> f64 {
|
||||||
|
[color.r, color.g, color.b]
|
||||||
|
.into_iter()
|
||||||
|
.zip([0.2126, 0.7152, 0.0722])
|
||||||
|
.map(|(channel, weight)| {
|
||||||
|
let channel = f64::from(channel);
|
||||||
|
weight
|
||||||
|
* if channel <= 0.04045 {
|
||||||
|
channel / 12.92
|
||||||
|
} else {
|
||||||
|
((channel + 0.055) / 1.055).powf(2.4)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contrast(first: Color, second: Color) -> f64 {
|
||||||
|
assert_eq!(first.a, 1.0, "opaque color contract: {first:?}");
|
||||||
|
assert_eq!(second.a, 1.0, "opaque color contract: {second:?}");
|
||||||
|
let first = luminance(first);
|
||||||
|
let second = luminance(second);
|
||||||
|
(first.max(second) + 0.05) / (first.min(second) + 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_contrast(label: &str, foreground: Color, background: Color, minimum: f64) {
|
||||||
|
let measured = contrast(foreground, background);
|
||||||
|
assert!(
|
||||||
|
measured >= minimum,
|
||||||
|
"{label}: {foreground:?} on {background:?} = {measured:.6}, required {minimum}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn solid(background: Background) -> Color {
|
||||||
|
match background {
|
||||||
|
Background::Color(color) => color,
|
||||||
|
Background::Gradient(_) => panic!("gradient needs a separate contrast evaluator"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semantic_text_and_essential_boundaries_have_contrast_on_canvas_surfaces() {
|
||||||
|
for surface in [
|
||||||
|
Color::WHITE,
|
||||||
|
BG,
|
||||||
|
Color::from_rgb8(246, 248, 251),
|
||||||
|
Color::from_rgb8(243, 248, 248),
|
||||||
|
Color::from_rgb8(234, 241, 248),
|
||||||
|
] {
|
||||||
|
require_contrast("primary text", INK, surface, 4.5);
|
||||||
|
require_contrast("secondary text", MUTED, surface, 4.5);
|
||||||
|
require_contrast("essential outline", BOUNDARY, surface, 3.0);
|
||||||
|
}
|
||||||
|
for kind in Kind::ALL {
|
||||||
|
let color = kind_color(kind);
|
||||||
|
// The architecture painter uses this six-percent encoded-channel tint.
|
||||||
|
let tint = Color::from_rgb(
|
||||||
|
0.94 + 0.06 * color.r,
|
||||||
|
0.94 + 0.06 * color.g,
|
||||||
|
0.94 + 0.06 * color.b,
|
||||||
|
);
|
||||||
|
require_contrast(&format!("{kind:?} header"), color, tint, 4.5);
|
||||||
|
}
|
||||||
|
// Negative controls: do not accidentally treat the decorative divider or
|
||||||
|
// the old secondary-text palette as adequate for these semantic roles.
|
||||||
|
assert!(contrast(LINE, Color::from_rgb8(243, 248, 248)) < 3.0);
|
||||||
|
assert!(contrast(Color::from_rgb(0.43, 0.48, 0.55), BG) < 4.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_editor_syntax_and_fallback_remain_readable_during_selection() {
|
||||||
|
let theme = theme();
|
||||||
|
for status in [
|
||||||
|
text_editor::Status::Active,
|
||||||
|
text_editor::Status::Hovered,
|
||||||
|
text_editor::Status::Focused { is_hovered: false },
|
||||||
|
text_editor::Status::Focused { is_hovered: true },
|
||||||
|
text_editor::Status::Disabled,
|
||||||
|
] {
|
||||||
|
let style = source_editor(&theme, status);
|
||||||
|
let surface = solid(style.background);
|
||||||
|
assert_eq!(surface, Color::WHITE);
|
||||||
|
require_contrast("source selected range", style.selection, surface, 3.0);
|
||||||
|
require_contrast("source boundary", style.border.color, surface, 3.0);
|
||||||
|
require_contrast("source placeholder", style.placeholder, surface, 4.5);
|
||||||
|
for foreground in [style.value, style.placeholder] {
|
||||||
|
require_contrast("source fallback", foreground, surface, 4.5);
|
||||||
|
require_contrast("selected fallback", foreground, style.selection, 4.5);
|
||||||
|
}
|
||||||
|
for class in [
|
||||||
|
TokenClass::Keyword,
|
||||||
|
TokenClass::Identifier,
|
||||||
|
TokenClass::String,
|
||||||
|
TokenClass::Number,
|
||||||
|
TokenClass::Comment,
|
||||||
|
TokenClass::Error,
|
||||||
|
TokenClass::Punctuation,
|
||||||
|
TokenClass::Whitespace,
|
||||||
|
] {
|
||||||
|
let format = crate::highlight::format(&class, &theme);
|
||||||
|
assert!(format.font.is_none(), "palette must preserve font metrics");
|
||||||
|
let foreground = format.color.expect("syntax class has explicit ink");
|
||||||
|
require_contrast(&format!("{class:?} ordinary"), foreground, surface, 4.5);
|
||||||
|
require_contrast(
|
||||||
|
&format!("{class:?} selected"),
|
||||||
|
foreground,
|
||||||
|
style.selection,
|
||||||
|
4.5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(contrast(Color::from_rgb8(32, 44, 58), SOURCE_SELECTION) < 4.5);
|
||||||
|
assert!(contrast(Color::from_rgb8(111, 121, 132), SOURCE_SELECTION) < 4.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_input_styles_keep_ordinary_values_and_visible_selection() {
|
||||||
|
let theme = theme();
|
||||||
|
for status in [
|
||||||
|
text_input::Status::Active,
|
||||||
|
text_input::Status::Hovered,
|
||||||
|
text_input::Status::Focused { is_hovered: false },
|
||||||
|
text_input::Status::Focused { is_hovered: true },
|
||||||
|
] {
|
||||||
|
let style = text_input::default(&theme, status);
|
||||||
|
let surface = solid(style.background);
|
||||||
|
assert_eq!(surface, BG, "preserve ordinary input surface");
|
||||||
|
assert_eq!(style.value, INK, "do not apply source syntax ink to inputs");
|
||||||
|
assert_ne!(style.selection, SOURCE_SELECTION);
|
||||||
|
require_contrast("input value", style.value, surface, 4.5);
|
||||||
|
require_contrast("input placeholder", style.placeholder, surface, 4.5);
|
||||||
|
require_contrast("input icon", style.icon, surface, 3.0);
|
||||||
|
require_contrast("input boundary", style.border.color, surface, 3.0);
|
||||||
|
require_contrast(
|
||||||
|
"input boundary exterior",
|
||||||
|
style.border.color,
|
||||||
|
Color::WHITE,
|
||||||
|
3.0,
|
||||||
|
);
|
||||||
|
require_contrast("input selection", style.selection, surface, 3.0);
|
||||||
|
require_contrast("input selected value", style.value, style.selection, 4.5);
|
||||||
|
}
|
||||||
|
let disabled = text_input::default(&theme, text_input::Status::Disabled);
|
||||||
|
let surface = solid(disabled.background);
|
||||||
|
require_contrast("disabled input value", disabled.value, surface, 4.5);
|
||||||
|
require_contrast(
|
||||||
|
"disabled input placeholder",
|
||||||
|
disabled.placeholder,
|
||||||
|
surface,
|
||||||
|
4.5,
|
||||||
|
);
|
||||||
|
// Selection interaction is unavailable while disabled; its unused selected
|
||||||
|
// value pair is not an active text contrast requirement.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_pick_list_and_menu_styles_cover_open_and_selected_states() {
|
||||||
|
let theme = theme();
|
||||||
|
for status in [
|
||||||
|
pick_list::Status::Active,
|
||||||
|
pick_list::Status::Hovered,
|
||||||
|
pick_list::Status::Opened { is_hovered: false },
|
||||||
|
pick_list::Status::Opened { is_hovered: true },
|
||||||
|
] {
|
||||||
|
let style = pick_list::default(&theme, status);
|
||||||
|
let surface = solid(style.background);
|
||||||
|
require_contrast("pick-list value", style.text_color, surface, 4.5);
|
||||||
|
require_contrast(
|
||||||
|
"pick-list placeholder",
|
||||||
|
style.placeholder_color,
|
||||||
|
surface,
|
||||||
|
4.5,
|
||||||
|
);
|
||||||
|
require_contrast("pick-list handle", style.handle_color, surface, 3.0);
|
||||||
|
require_contrast("pick-list boundary", style.border.color, surface, 3.0);
|
||||||
|
require_contrast("pick-list exterior", style.border.color, BG, 3.0);
|
||||||
|
}
|
||||||
|
let style = menu::default(&theme);
|
||||||
|
let surface = solid(style.background);
|
||||||
|
let selection = solid(style.selected_background);
|
||||||
|
require_contrast("menu text", style.text_color, surface, 4.5);
|
||||||
|
require_contrast("menu boundary", style.border.color, surface, 3.0);
|
||||||
|
require_contrast("menu selected row", selection, surface, 3.0);
|
||||||
|
require_contrast(
|
||||||
|
"menu selected text",
|
||||||
|
style.selected_text_color,
|
||||||
|
selection,
|
||||||
|
4.5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_checkbox_styles_keep_checks_and_outlines_distinct() {
|
||||||
|
let theme = theme();
|
||||||
|
for checked in [false, true] {
|
||||||
|
for status in [
|
||||||
|
checkbox::Status::Active {
|
||||||
|
is_checked: checked,
|
||||||
|
},
|
||||||
|
checkbox::Status::Hovered {
|
||||||
|
is_checked: checked,
|
||||||
|
},
|
||||||
|
] {
|
||||||
|
let style = checkbox::primary(&theme, status);
|
||||||
|
let surface = solid(style.background);
|
||||||
|
require_contrast("checkbox boundary exterior", style.border.color, BG, 3.0);
|
||||||
|
require_contrast("checkbox label", style.text_color.unwrap_or(INK), BG, 4.5);
|
||||||
|
if checked {
|
||||||
|
require_contrast("checkbox check", style.icon_color, surface, 3.0);
|
||||||
|
} else {
|
||||||
|
require_contrast(
|
||||||
|
"unchecked checkbox boundary",
|
||||||
|
style.border.color,
|
||||||
|
surface,
|
||||||
|
3.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let disabled = checkbox::primary(&theme, checkbox::Status::Disabled { is_checked: true });
|
||||||
|
require_contrast(
|
||||||
|
"disabled check",
|
||||||
|
disabled.icon_color,
|
||||||
|
solid(disabled.background),
|
||||||
|
3.0,
|
||||||
|
);
|
||||||
|
// The disabled unchecked outline is intentionally exempt from the active
|
||||||
|
// control benchmark. It remains an inactive widget, not a missing checked cue.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn button_pair_couplings_cover_hover_and_disabled_states() {
|
||||||
|
let theme = theme();
|
||||||
|
for (name, resolve) in [
|
||||||
|
(
|
||||||
|
"primary",
|
||||||
|
button::primary as fn(&Theme, button::Status) -> button::Style,
|
||||||
|
),
|
||||||
|
("secondary", button::secondary),
|
||||||
|
("success", button::success),
|
||||||
|
("warning", button::warning),
|
||||||
|
("danger", button::danger),
|
||||||
|
] {
|
||||||
|
for status in [
|
||||||
|
button::Status::Active,
|
||||||
|
button::Status::Hovered,
|
||||||
|
button::Status::Pressed,
|
||||||
|
] {
|
||||||
|
let style = resolve(&theme, status);
|
||||||
|
let surface = solid(style.background.expect("filled button"));
|
||||||
|
require_contrast(name, style.text_color, surface, 4.5);
|
||||||
|
require_contrast("button silhouette", surface, BG, 3.0);
|
||||||
|
}
|
||||||
|
let disabled = resolve(&theme, button::Status::Disabled);
|
||||||
|
// Iced composites disabled colors with alpha; do not feed them into an
|
||||||
|
// opaque-pair contrast test or claim active-state contrast compliance.
|
||||||
|
assert!(disabled.text_color.a > 0.0 && disabled.text_color.a < 1.0);
|
||||||
|
let background = solid(disabled.background.expect("disabled filled button"));
|
||||||
|
assert!(background.a > 0.0 && background.a < 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selected_navigation_adds_a_visible_outline_and_preserves_disabled_ink() {
|
||||||
|
let theme = theme();
|
||||||
|
for status in [
|
||||||
|
button::Status::Active,
|
||||||
|
button::Status::Hovered,
|
||||||
|
button::Status::Pressed,
|
||||||
|
] {
|
||||||
|
let ordinary = nav(&theme, status);
|
||||||
|
let chosen = selected(&theme, status);
|
||||||
|
let surface = solid(chosen.background.expect("selected navigation fill"));
|
||||||
|
assert!(chosen.border.width >= 2.0 && chosen.border.width > ordinary.border.width);
|
||||||
|
require_contrast("selected navigation text", chosen.text_color, surface, 4.5);
|
||||||
|
require_contrast(
|
||||||
|
"selected navigation interior",
|
||||||
|
chosen.border.color,
|
||||||
|
surface,
|
||||||
|
3.0,
|
||||||
|
);
|
||||||
|
require_contrast(
|
||||||
|
"selected navigation exterior",
|
||||||
|
chosen.border.color,
|
||||||
|
Color::WHITE,
|
||||||
|
3.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let disabled = selected(&theme, button::Status::Disabled);
|
||||||
|
assert!(disabled.text_color.a > 0.0 && disabled.text_color.a < 1.0);
|
||||||
|
}
|
||||||
@@ -1186,6 +1186,7 @@ fn source(app: &App) -> View<'_, Message> {
|
|||||||
}
|
}
|
||||||
let editor = widget::text_editor(&app.source_editor)
|
let editor = widget::text_editor(&app.source_editor)
|
||||||
.id("source-editor")
|
.id("source-editor")
|
||||||
|
.style(style::source_editor)
|
||||||
.on_action(Message::SourceEdit)
|
.on_action(Message::SourceEdit)
|
||||||
.font(Font::MONOSPACE)
|
.font(Font::MONOSPACE)
|
||||||
.size(14)
|
.size(14)
|
||||||
|
|||||||
Reference in New Issue
Block a user