use denise::theme::{AA_LARGE, derive_content};
use denise::{Color, Point, Rect, Role, Size, Theme};
use denise_render::Canvas;
use denise_text::{TextEngine, TextStyle};
use crate::widget::VisualState;
const HOVER_MIX: u8 = 24;
const PRESS_MIX: u8 = 64;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Align {
#[default]
Start,
Center,
End,
}
impl Align {
#[inline]
pub const fn offset(self, available: i32, content: i32) -> i32 {
match self {
Align::Start => 0,
Align::Center => (available - content) / 2,
Align::End => available - content,
}
}
}
pub(crate) fn interactive_pair(theme: &Theme, role: Role, state: VisualState) -> (Color, Color) {
let (background, content) = theme.pair(role);
if state.contains(VisualState::DISABLED) {
let background = theme.color(Role::Base200);
return (background, derive_content(background, AA_LARGE));
}
if state.contains(VisualState::PRESSED) {
return (background.mix(content, PRESS_MIX), content);
}
if state.contains(VisualState::HOVERED) {
return (background.mix(content, HOVER_MIX), content);
}
(background, content)
}
pub(crate) fn focus_ring(theme: &Theme, bounds: Rect, radius: i32, canvas: &mut Canvas<'_>) {
canvas.stroke_rounded_rect(
bounds.inflate(-1),
(radius - 1).max(0),
2,
theme.color(Role::Accent),
);
}
pub(crate) fn draw_aligned(
canvas: &mut Canvas<'_>,
engine: &mut TextEngine,
style: TextStyle,
bounds: Rect,
align: (Align, Align),
text: &str,
color: Color,
) -> Size {
let extent = engine.measure(style, text);
let at = Point::new(
bounds.x + align.0.offset(bounds.width, extent.width as i32),
bounds.y + align.1.offset(bounds.height, extent.height as i32),
);
engine.draw(canvas, style, at, text, color);
extent
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alignment_offsets() {
assert_eq!(Align::Start.offset(100, 20), 0);
assert_eq!(Align::Center.offset(100, 20), 40);
assert_eq!(Align::End.offset(100, 20), 80);
assert_eq!(Align::Center.offset(20, 100), -40);
}
#[test]
fn every_state_keeps_the_pair_readable() {
for theme in Theme::BUILT_IN {
for role in [Role::Primary, Role::Secondary, Role::Accent, Role::Error] {
for state in [
VisualState::NONE,
VisualState::HOVERED,
VisualState::PRESSED,
VisualState::DISABLED,
] {
let (background, content) = interactive_pair(&theme, role, state);
let ratio = denise::theme::contrast_x100(background, content);
assert!(
ratio >= AA_LARGE,
"{} {role:?} {state:?} is {ratio} against a floor of {AA_LARGE}",
theme.name
);
}
}
}
}
}