use crate::modifier::Color;
use crate::text::paragraph::TextAlign;
use crate::text::style::{
LineHeightAlignment, LineHeightMode, LineHeightStyle, LineHeightTrim, ParagraphStyle,
PlatformParagraphStyle, SpanStyle, TextStyle,
};
use crate::text::{FontFamily, FontWeight, TextUnit};
use crate::widgets::wear::color_appearance::set_luminance;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WearColors {
pub primary: Color,
pub primary_container: Color,
pub on_primary: Color,
pub on_primary_container: Color,
pub surface_container: Color,
pub on_surface: Color,
pub on_surface_variant: Color,
pub outline: Color,
pub background: Color,
pub on_background: Color,
pub content: Color,
pub indicator_thumb: Color,
pub indicator_track: Color,
}
impl Default for WearColors {
fn default() -> Self {
Self {
primary: Color::from_rgb_u8(0xA8, 0xC7, 0xFA),
primary_container: Color::from_rgb_u8(0x0B, 0x57, 0xD0),
on_primary: Color::from_rgb_u8(0x00, 0x00, 0x00),
on_primary_container: Color::from_rgb_u8(0xD3, 0xE3, 0xFD),
surface_container: Color::from_rgb_u8(0x1E, 0x1F, 0x20),
on_surface: Color::from_rgb_u8(0xE3, 0xE3, 0xE3),
on_surface_variant: Color::from_rgb_u8(0xC4, 0xC7, 0xC5),
outline: Color::from_rgb_u8(0x8E, 0x91, 0x8F),
background: Color::from_rgb_u8(0x00, 0x00, 0x00),
on_background: Color::from_rgb_u8(0xE3, 0xE3, 0xE3),
content: Color::WHITE,
indicator_thumb: Color::WHITE,
indicator_track: Color::WHITE,
}
.with_wear_scroll_indicator()
}
}
impl WearColors {
pub fn with_wear_scroll_indicator(mut self) -> Self {
self.indicator_thumb = set_luminance(self.on_background, 80.0);
self.indicator_track = set_luminance(self.on_background, 20.0);
self
}
}
pub fn wear_line_height_style() -> LineHeightStyle {
LineHeightStyle {
alignment: LineHeightAlignment::Center,
trim: LineHeightTrim::None,
mode: LineHeightMode::Minimum,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WearTextStyle {
pub size_sp: f32,
pub line_height_sp: f32,
pub weight: u16,
pub tracking_sp: f32,
pub align: TextAlign,
}
impl WearTextStyle {
pub const BRAND_FAMILY: FontFamily = FontFamily::SansSerif;
pub const TITLE_MEDIUM: Self = Self {
size_sp: 16.0,
line_height_sp: 18.0,
weight: 550,
tracking_sp: 0.4,
align: TextAlign::Unspecified,
};
pub const LABEL_MEDIUM: Self = Self {
size_sp: 15.0,
line_height_sp: 18.0,
weight: 500,
tracking_sp: 0.4,
align: TextAlign::Unspecified,
};
pub const LABEL_SMALL: Self = Self {
size_sp: 13.0,
line_height_sp: 16.0,
weight: 500,
tracking_sp: 0.4,
align: TextAlign::Unspecified,
};
pub const BODY_LARGE: Self = Self {
size_sp: 16.0,
line_height_sp: 18.0,
weight: 450,
tracking_sp: 0.4,
align: TextAlign::Unspecified,
};
pub const fn at_size(self, size_sp: f32) -> Self {
Self { size_sp, ..self }
}
pub const fn with_line_height(self, line_height_sp: f32) -> Self {
Self {
line_height_sp,
..self
}
}
pub const fn aligned(self, align: TextAlign) -> Self {
Self { align, ..self }
}
pub fn resolve(self, color: Color) -> TextStyle {
self.resolve_in(color, Self::BRAND_FAMILY)
}
pub fn resolve_in(self, color: Color, family: FontFamily) -> TextStyle {
TextStyle {
span_style: SpanStyle {
color: Some(color),
font_size: TextUnit::Sp(self.size_sp),
font_weight: Some(FontWeight(self.weight)),
letter_spacing: TextUnit::Sp(self.tracking_sp),
font_family: Some(family),
..SpanStyle::default()
},
paragraph_style: ParagraphStyle {
line_height: TextUnit::Sp(self.line_height_sp),
text_align: self.align,
line_height_style: Some(wear_line_height_style()),
platform_style: Some(PlatformParagraphStyle {
include_font_padding: Some(false),
shaping: None,
}),
..ParagraphStyle::default()
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_indicator_colours_come_from_on_background_and_nothing_else() {
let colors = WearColors {
on_background: Color::from_rgb_u8(0xDF, 0xF6, 0xFF),
..WearColors::default()
}
.with_wear_scroll_indicator();
assert_eq!(colors.indicator_thumb, Color::from_rgb_u8(180, 202, 211));
assert_eq!(colors.indicator_track, Color::from_rgb_u8(30, 51, 58));
let default = WearColors::default();
assert_eq!(default, default.with_wear_scroll_indicator());
}
#[test]
fn a_bare_text_is_white_and_a_header_is_not() {
let colors = WearColors::default();
assert_eq!(colors.content, Color::WHITE);
assert_ne!(colors.content, colors.on_background);
}
#[test]
fn the_type_scale_carries_the_sizes_wear_declares() {
assert_eq!(WearTextStyle::TITLE_MEDIUM.size_sp, 16.0);
assert_eq!(WearTextStyle::TITLE_MEDIUM.line_height_sp, 18.0);
assert_eq!(WearTextStyle::TITLE_MEDIUM.weight, 550);
assert_eq!(WearTextStyle::LABEL_MEDIUM.size_sp, 15.0);
assert_eq!(WearTextStyle::LABEL_SMALL.size_sp, 13.0);
assert_eq!(WearTextStyle::LABEL_SMALL.line_height_sp, 16.0);
assert_eq!(WearTextStyle::BODY_LARGE.weight, 450);
for style in [
WearTextStyle::TITLE_MEDIUM,
WearTextStyle::LABEL_MEDIUM,
WearTextStyle::LABEL_SMALL,
WearTextStyle::BODY_LARGE,
] {
assert_eq!(style.tracking_sp, 0.4);
}
}
#[test]
fn overriding_the_size_keeps_the_line_height_it_inherited() {
let small = WearTextStyle::BODY_LARGE.at_size(12.0);
assert_eq!(small.size_sp, 12.0);
assert_eq!(small.line_height_sp, 18.0);
let credit_line = small.with_line_height(16.0);
assert_eq!(credit_line.line_height_sp, 16.0);
}
#[test]
fn the_scale_itself_states_no_alignment_and_a_call_site_can() {
for style in [
WearTextStyle::TITLE_MEDIUM,
WearTextStyle::LABEL_MEDIUM,
WearTextStyle::LABEL_SMALL,
WearTextStyle::BODY_LARGE,
] {
assert_eq!(style.align, TextAlign::Unspecified);
assert_eq!(
style.resolve(Color::WHITE).paragraph_style.text_align,
TextAlign::Unspecified
);
}
let centred = WearTextStyle::BODY_LARGE
.at_size(12.0)
.with_line_height(16.0)
.aligned(TextAlign::Center);
assert_eq!(centred.align, TextAlign::Center);
assert_eq!(
centred.resolve(Color::WHITE).paragraph_style.text_align,
TextAlign::Center
);
assert_eq!(centred.size_sp, 12.0);
assert_eq!(centred.line_height_sp, 16.0);
assert_eq!(centred.weight, WearTextStyle::BODY_LARGE.weight);
}
#[test]
fn every_entry_names_a_family_so_its_text_has_a_face_to_draw_with() {
for style in [
WearTextStyle::TITLE_MEDIUM,
WearTextStyle::LABEL_MEDIUM,
WearTextStyle::LABEL_SMALL,
WearTextStyle::BODY_LARGE,
] {
assert_eq!(
style.resolve(Color::WHITE).span_style.font_family,
Some(FontFamily::SansSerif),
"Wear's brand token resolves to sans-serif on a device with no \
RobotoFlex-Regular.ttf, which is every Wear OS 5 image measured"
);
}
assert_eq!(
WearTextStyle::BODY_LARGE
.resolve_in(Color::WHITE, FontFamily::Monospace)
.span_style
.font_family,
Some(FontFamily::Monospace),
"a caller can still name its own"
);
}
#[test]
fn a_resolved_style_keeps_its_sizes_in_sp_for_the_framework_to_scale() {
let style = WearTextStyle::LABEL_MEDIUM.resolve(Color::WHITE);
assert_eq!(style.span_style.font_size, TextUnit::Sp(15.0));
assert_eq!(style.paragraph_style.line_height, TextUnit::Sp(18.0));
assert_eq!(style.span_style.font_weight, Some(FontWeight(500)));
assert_eq!(style.span_style.letter_spacing, TextUnit::Sp(0.4));
}
#[test]
fn a_resolved_style_asks_for_the_wear_line_box_rule() {
let style = WearTextStyle::TITLE_MEDIUM.resolve(Color::WHITE);
let policy = style
.paragraph_style
.line_height_style
.expect("a Wear style names its line-height policy");
assert_eq!(policy.alignment, LineHeightAlignment::Center);
assert_eq!(policy.trim, LineHeightTrim::None);
assert_eq!(policy.mode, LineHeightMode::Minimum);
assert_eq!(
style
.paragraph_style
.platform_style
.and_then(|platform| platform.include_font_padding),
Some(false)
);
}
}