use crate::core::{Color, Font, Rect, Size};
use crate::render::text::{estimate_cluster_advance, for_each_cluster};
use crate::style::{EdgeOffsets, WidgetStyle};
#[cfg(all(feature = "serde", widgets_unstripped))]
use serde::{Deserialize, Serialize};
#[cfg(feature = "text-shaping")]
thread_local! {
static CLUSTER_RANGES: core::cell::RefCell<crate::compat::Vec<(usize, usize)>> =
const { core::cell::RefCell::new(crate::compat::Vec::new()) };
}
#[cfg(feature = "text-shaping")]
fn with_cluster_ranges<R>(text: &str, f: impl FnOnce(&[(usize, usize)]) -> R) -> R {
CLUSTER_RANGES.with(|cell| match cell.try_borrow_mut() {
Ok(mut ranges) => {
ranges.clear();
for_each_cluster(text, |_cluster, range| ranges.push(range));
f(&ranges)
}
Err(_) => {
let mut owned: crate::compat::Vec<(usize, usize)> = crate::compat::Vec::new();
for_each_cluster(text, |_cluster, range| owned.push(range));
f(&owned)
}
})
}
fn flat_cluster_model(text: &str, size: f32, scale: f32) -> (f32, usize) {
let mut total = 0.0f32;
let mut clusters = 0usize;
for_each_cluster(text, |cluster, _range| {
total += estimate_cluster_advance(cluster, size, scale);
clusters += 1;
});
(total, clusters)
}
pub fn estimate_text_width(text: &str, font: &crate::core::Font, scale: f32) -> u32 {
let size = font.size().max(0.0);
if text.is_empty() || size == 0.0 {
return 0;
}
#[cfg(feature = "text-shaping")]
let (mut advance, clusters) = {
let mut clusters = 0usize;
let shaped = with_cluster_ranges(text, |ranges| {
clusters = ranges.len();
crate::render::text::shaping::cluster_advances(text, ranges, font, scale).filter(
|advances| advances.len() == clusters,
)
});
match shaped {
Some(advances) => (advances.iter().sum::<f32>(), clusters),
None => flat_cluster_model(text, size, scale),
}
};
#[cfg(not(feature = "text-shaping"))]
let (mut advance, clusters) = flat_cluster_model(text, size, scale);
let tracking = font.letter_spacing() * scale;
if tracking != 0.0 && clusters > 1 {
advance += tracking * (clusters - 1) as f32;
}
advance.round().max(0.0) as u32
}
pub fn estimate_line_height(font: &crate::core::Font, scale: f32) -> u32 {
(font.effective_line_height().max(1.0) * scale).round().max(1.0) as u32
}
pub fn effective_font(style: &WidgetStyle) -> &Font {
fn default_font() -> &'static Font {
use crate::compat::OnceLock;
static DEFAULT: OnceLock<Font> = OnceLock::new();
DEFAULT.get_or_init(Font::default)
}
match style.font.as_ref() {
Some(font) => font,
None => default_font(),
}
}
pub fn role_font(style: &WidgetStyle, role_size: f32) -> Font {
let effective = effective_font(style);
let base = Font::default().size().max(1.0);
let scaled = role_size.max(0.0) * (effective.size().max(1.0) / base);
effective.with_size(scaled)
}
pub fn lerp_u32(from: u32, to: u32, t: f32) -> u32 {
lerp_f32(from as f32, to as f32, t).round().max(0.0) as u32
}
pub fn lerp_i32(from: i32, to: i32, t: f32) -> i32 {
lerp_f32(from as f32, to as f32, t).round() as i32
}
pub fn lerp_f32(from: f32, to: f32, t: f32) -> f32 {
if t <= 0.0 {
return from;
}
if t >= 1.0 {
return to;
}
from + (to - from) * t
}
pub struct ControlMetrics;
pub fn switch_hint_size(track: crate::core::Size, focus_ring_width: u32) -> crate::core::Size {
crate::core::Size::new(
track.width.saturating_add(focus_ring_width.saturating_mul(2)),
track.height,
)
}
impl ControlMetrics {
pub fn implicit_size(content: Size, padding: EdgeOffsets, floor: Size) -> Size {
let padded_width = content.width.saturating_add(padding.horizontal_total());
let padded_height = content.height.saturating_add(padding.vertical_total());
Size::new(padded_width.max(floor.width), padded_height.max(floor.height))
}
pub fn content_box(rect: Rect, padding: EdgeOffsets) -> Rect {
Rect::new(
rect.x.saturating_add(padding.left as i32),
rect.y.saturating_add(padding.top as i32),
rect.width.saturating_sub(padding.horizontal_total()),
rect.height.saturating_sub(padding.vertical_total()),
)
}
pub fn center_in(rect: Rect, floor: Size) -> Rect {
let width = floor.width.min(rect.width);
let height = floor.height.min(rect.height);
Rect::new(
rect.x + (rect.width.saturating_sub(width) / 2) as i32,
rect.y + (rect.height.saturating_sub(height) / 2) as i32,
width,
height,
)
}
pub fn leading_box(rect: Rect, size: Size, padding: EdgeOffsets) -> Rect {
let width = size.width.min(rect.width);
let height = size.height;
let y = rect.y + (rect.height.saturating_sub(height) / 2) as i32;
let x = rect.x.saturating_add(padding.left as i32);
let x = x.min(rect.x + rect.width.saturating_sub(width) as i32);
Rect::new(x, y, width, height)
}
pub fn centered_disc(rect: Rect, diameter: u32) -> Rect {
let diameter = diameter.min(rect.width).min(rect.height);
Rect::new(
rect.x + (rect.width.saturating_sub(diameter) / 2) as i32,
rect.y + (rect.height.saturating_sub(diameter) / 2) as i32,
diameter,
diameter,
)
}
pub fn centered_band(rect: Rect, height: u32) -> Rect {
let height = height.min(rect.height);
Rect::new(
rect.x,
rect.y + (rect.height.saturating_sub(height) / 2) as i32,
rect.width,
height,
)
}
pub fn full_width_band(rect: Rect, height: u32) -> Rect {
Self::centered_band(rect, height)
}
pub fn centered_square(rect: Rect, size: u32) -> Rect {
Self::center_in(rect, Size::new(size, size))
}
pub fn top_band(rect: Rect, height: u32) -> Rect {
Rect::new(rect.x, rect.y, rect.width, height.min(rect.height))
}
pub fn bottom_band(rect: Rect, height: u32) -> Rect {
let height = height.min(rect.height);
Rect::new(rect.x, rect.y + rect.height.saturating_sub(height) as i32, rect.width, height)
}
pub fn band_inset(band: Rect, inset: u32) -> Rect {
Rect::new(
band.x.saturating_add(inset as i32),
band.y.saturating_add(inset as i32),
band.width.saturating_sub(2 * inset),
band.height.saturating_sub(2 * inset),
)
}
pub fn content_above_bottom_band(rect: Rect, height: u32) -> Rect {
let height = height.min(rect.height);
Rect::new(rect.x, rect.y, rect.width, rect.height.saturating_sub(height))
}
pub fn content_below_top_band(rect: Rect, height: u32) -> Rect {
let height = height.min(rect.height);
Rect::new(
rect.x,
rect.y.saturating_add(height as i32),
rect.width,
rect.height.saturating_sub(height),
)
}
pub fn painted_box(rect: Rect, intrinsic: Size) -> Rect {
let boxed = Self::center_in(rect, intrinsic);
Rect::new(boxed.x, boxed.y, boxed.width.max(1), boxed.height.max(1))
}
pub fn focus_ring_rect(rect: Rect) -> Rect {
let inset = FOCUS_RING_WIDTH as i32;
Rect::new(
rect.x.saturating_add(inset),
rect.y.saturating_add(inset),
rect.width.saturating_sub(2 * FOCUS_RING_WIDTH),
rect.height.saturating_sub(2 * FOCUS_RING_WIDTH),
)
}
pub fn focus_ring_radius(radius: u32) -> u32 {
radius.saturating_sub(FOCUS_RING_WIDTH)
}
pub fn sub_control_rect(rect: Rect, part: SubControl, metrics: SubControlMetrics) -> Rect {
let leading = metrics.leading_width.min(rect.width);
let trailing = metrics.trailing_width.min(rect.width.saturating_sub(leading));
let leading_gap = if leading > 0 { metrics.gap } else { 0 };
let trailing_gap = if trailing > 0 { metrics.gap } else { 0 };
let inner_left = leading.saturating_add(leading_gap);
let inner_width = rect
.width
.saturating_sub(inner_left.saturating_add(trailing).saturating_add(trailing_gap));
let trailing_left = rect.width.saturating_sub(trailing) as i32;
let upper = rect.height.div_ceil(2);
match part {
SubControl::Contents => {
Rect::new(rect.x + inner_left as i32, rect.y, inner_width, rect.height)
}
SubControl::Leading => Rect::new(rect.x, rect.y, leading, rect.height),
SubControl::Trailing => {
Rect::new(rect.x + trailing_left, rect.y, trailing, rect.height)
}
SubControl::Increment => Rect::new(rect.x + trailing_left, rect.y, trailing, upper),
SubControl::Decrement => Rect::new(
rect.x + trailing_left,
rect.y + upper as i32,
trailing,
rect.height.saturating_sub(upper),
),
SubControl::Close => {
let side = metrics.close_size.min(rect.width).min(rect.height);
Rect::new(
rect.x + rect.width.saturating_sub(side) as i32,
rect.y + (rect.height.saturating_sub(side) / 2) as i32,
side,
side,
)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubControl {
Contents,
Leading,
Trailing,
Increment,
Decrement,
Close,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SubControlMetrics {
pub leading_width: u32,
pub trailing_width: u32,
pub close_size: u32,
pub gap: u32,
}
impl SubControlMetrics {
pub const NONE: Self = Self { leading_width: 0, trailing_width: 0, close_size: 0, gap: 0 };
pub const fn with_leading(mut self, width: u32) -> Self {
self.leading_width = width;
self
}
pub const fn with_trailing(mut self, width: u32) -> Self {
self.trailing_width = width;
self
}
pub const fn with_close(mut self, side: u32) -> Self {
self.close_size = side;
self
}
pub const fn with_gap(mut self, gap: u32) -> Self {
self.gap = gap;
self
}
}
pub const FOCUS_RING_WIDTH: u32 = 2;
pub const FOCUS_HALO_WIDTH: u32 = 4;
pub const FOCUS_HALO_BLUR_RADIUS: u32 = 6;
pub const DAMAGE_MARGIN: u32 =
if FOCUS_RING_WIDTH > FOCUS_HALO_WIDTH { FOCUS_RING_WIDTH } else { FOCUS_HALO_WIDTH }
+ FOCUS_HALO_BLUR_RADIUS;
pub const OUTLINE_PANEL_MIN_EDITOR_WIDTH: u32 = 240;
pub const QUOTE_BOARD_COMPACT_CELL_WIDTH: u32 = 420;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusRingStyle {
Inner,
Halo,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FocusRing {
pub rect: Rect,
pub radius: u32,
}
impl FocusRing {
pub fn for_control(rect: Rect, radius: u32) -> Self {
Self {
rect: ControlMetrics::focus_ring_rect(rect),
radius: ControlMetrics::focus_ring_radius(radius),
}
}
pub fn is_drawable(&self) -> bool {
self.rect.width > 0 && self.rect.height > 0
}
pub fn halo(rect: Rect, radius: u32) -> Self {
Self::grown_by(rect, radius, FOCUS_RING_WIDTH)
}
pub fn halo_for_control(rect: Rect, radius: u32) -> Self {
Self::grown_by(rect, radius, FOCUS_HALO_WIDTH)
}
fn grown_by(rect: Rect, radius: u32, width: u32) -> Self {
let grow = width as i32;
Self {
rect: Rect::new(
rect.x.saturating_sub(grow),
rect.y.saturating_sub(grow),
rect.width.saturating_add(2 * width),
rect.height.saturating_add(2 * width),
),
radius: radius.saturating_add(width),
}
}
}
pub fn focus_ring_color(fallback: Color) -> Color {
ring_color_from(
crate::style::theme_manager()
.current_theme()
.map(|theme| focus_ring_color_from_theme(&theme.colors)),
fallback,
)
}
pub fn focus_ring_color_from_theme(colors: &crate::style::Colors) -> Color {
colors.outline
}
fn ring_color_from(outline: Option<Color>, fallback: Color) -> Color {
outline.unwrap_or_else(|| fallback.contrast_color())
}
pub fn minimum_hit_size() -> Size {
crate::layout::LayoutContext::default().min_touch_size
}
pub fn expand_to_hit_area(rect: Rect) -> Rect {
crate::layout::grow_to_min_touch_size(rect, minimum_hit_size())
}
pub mod dimensions {
use crate::core::Size;
pub const TOUCH_TARGET_MIN: u32 = 48;
pub const BUTTON_MIN: Size = Size { width: 64, height: 40 };
pub const BUTTON_PADDING_H: u32 = 12;
pub const BUTTON_PADDING_V: u32 = 8;
pub const BUTTON_ICON_SPACING: u32 = 6;
pub const BUTTON_ICON_SIZE: u32 = 18;
pub const BUTTON_RADIUS: u32 = 4;
pub const SWITCH_TRACK: Size = Size { width: 52, height: 32 };
pub const SWITCH_THUMB_RADIUS: u32 = 14;
pub const SWITCH_THUMB_INSET: u32 = 2;
pub const CUPERTINO_SWITCH_TRACK: Size = Size { width: 51, height: 31 };
pub const CUPERTINO_SWITCH_THUMB_RADIUS: u32 = 13;
pub const CUPERTINO_SWITCH_PRESS_STRETCH: u32 = 7;
pub const COLLAPSIBLE_HEADER_HEIGHT: u32 = 44;
pub const CHECKBOX_BOX: u32 = 18;
pub const CHECKBOX_RADIUS: u32 = 2;
pub const CHECKBOX_STROKE: u32 = 2;
pub const RADIO_OUTER_RADIUS: u32 = 8;
pub const RADIO_DOT_RADIUS: u32 = 5;
pub const RADIO_STROKE: u32 = 2;
pub const INDICATOR_TEXT_SPACING: u32 = 6;
pub const PROGRESS_HEIGHT: u32 = 4;
pub const PROGRESS_RADIUS: u32 = 2;
pub const PROGRESS_CIRCLE_STROKE: u32 = 4;
pub const SPINNER_DIAMETER: u32 = 48;
pub const RATING_ROW_HEIGHT: u32 = 24;
pub const RATING_STAR_SIZE: u32 = 24;
pub const RATING_STAR_GAP: u32 = 4;
pub const SLIDER_TRACK_HEIGHT: u32 = 4;
pub const SLIDER_TRACK_RADIUS: u32 = 2;
pub const SLIDER_THUMB_RADIUS: u32 = 10;
pub const TEXT_FIELD_MIN_HEIGHT: u32 = 48;
pub const TEXT_FIELD_PADDING_H: u32 = 12;
pub const CARD_RADIUS: u32 = 12;
pub const DIALOG_RADIUS: u32 = 28;
pub const DIALOG_MIN_WIDTH: u32 = 280;
pub const DIALOG_PADDING: u32 = 12;
pub const DIALOG_TITLE_BAR_HEIGHT: u32 = 28;
pub const DIALOG_BUTTON_HEIGHT: u32 = 28;
pub const DIALOG_MIN_HEIGHT: u32 = 240;
pub const TOOLBAR_HEIGHT: u32 = 56;
pub const TOOLBAR_SPACING: u32 = 6;
pub const TOOLBAR_ITEM_INSET: u32 = 2;
pub const MENU_BAR_HEIGHT: u32 = 28;
pub const STATUS_BAR_HEIGHT: u32 = 24;
pub const TAB_HEIGHT: u32 = 24;
pub const TAB_MIN_WIDTH: u32 = 40;
pub const TAB_MAX_WIDTH: u32 = 200;
pub const TAB_SPACING: u32 = 2;
pub const TAB_TEXT_PADDING: u32 = 24;
pub const PAGINATION_HEIGHT: u32 = 32;
pub const APP_BAR_HEIGHT: u32 = 56;
pub const BOTTOM_NAV_HEIGHT: u32 = 56;
pub const SPLITTER_HANDLE_THICKNESS: u32 = 5;
pub const MENU_HEADING_HEIGHT: u32 = 20;
pub const MENU_POPUP_PADDING: u32 = 2;
pub const MENU_SEPARATOR_HEIGHT: u32 = 6;
pub const PANE_HEADER_HEIGHT: u32 = 24;
pub const SCROLLBAR_THICKNESS: u32 = 8;
pub const SCROLLBAR_MIN_LENGTH: u32 = 48;
pub const SCROLLBAR_OVERLAY_THICKNESS: u32 = 6;
pub const SCROLLBAR_OVERLAY_HOVER_THICKNESS: u32 = 9;
pub const DIVIDER_THICKNESS: u32 = 1;
pub const DIVIDER_SPACING: u32 = 16;
pub const TOOLTIP_HEIGHT: u32 = 24;
pub const TOOLTIP_PADDING_H: u32 = 8;
pub const TOOLTIP_PADDING_V: u32 = 4;
pub const FONT_SIZE_BASE: u32 = 14;
pub const AVATAR_SIZE: u32 = 40;
pub const CHIP_HEIGHT: u32 = 32;
pub const CHIP_PADDING_H: u32 = 8;
pub const SEGMENTED_CONTROL_HEIGHT: u32 = 32;
pub const PICKER_ROW_HEIGHT: u32 = 32;
pub const PICKER_VISIBLE_ROWS: u32 = 5;
pub const NAV_BAR_HEIGHT: u32 = 44;
pub const NAV_BAR_LARGE_HEIGHT: u32 = 96;
pub const WEB_URL_BAR_HEIGHT: u32 = 28;
pub const BREADCRUMB_HEIGHT: u32 = 28;
pub const SPLIT_BUTTON_HEIGHT: u32 = 28;
pub const SPLIT_ARROW_COLUMN_WIDTH: u32 = 22;
pub const SPLIT_BUTTON_PADDING_H: u32 = 8;
pub const MENU_ROW_PADDING_H: u32 = 8;
pub const MENU_ROW_TRAILING_WIDTH: u32 = 28;
pub const MENU_ROW_HEIGHT: u32 = 22;
pub const STATUS_BAR_PADDING_H: u32 = 6;
pub const STATUS_GRIP_SIZE: u32 = 12;
pub const SNACKBAR_HEIGHT: u32 = 48;
pub const TOAST_HEIGHT: u32 = 48;
pub const TOAST_ACCENT_WIDTH: u32 = 4;
pub const CHART_LABEL_GUTTER: i32 = 120;
pub const CHART_TRACK_MARGIN: i32 = 10;
pub const VIDEO_CONTROL_BAR_HEIGHT: u32 = 36;
pub const VIDEO_SEEK_BAR_HEIGHT: u32 = 8;
pub const ROLLER_VISIBLE_ROWS: u32 = 5;
pub const ROLLER_ROW_HEIGHT: u32 = 28;
pub const ROLLER_WHEEL_HEIGHT: u32 = ROLLER_VISIBLE_ROWS * ROLLER_ROW_HEIGHT;
pub const STEPPER_ROW_HEIGHT: u32 = TOUCH_TARGET_MIN;
pub const STEPPER_BUTTON_WIDTH: u32 = TOUCH_TARGET_MIN;
pub const STEPPER_PADDING: u32 = 2;
pub const SPIN_BOX_STEP_BUTTON_WIDTH: u32 = 20;
pub const SPIN_BOX_STEP_BUTTONS: u32 = 2;
pub const SEARCH_BOX_FIELD_HEIGHT: u32 = TEXT_FIELD_MIN_HEIGHT;
pub const BADGE_PILL_HEIGHT: u32 = 18;
pub const BADGE_PILL_PADDING_H: u32 = 6;
pub const BADGE_PILL_PADDING_V: u32 = 2;
pub const BADGE_LABEL_FONT_SIZE: u32 = 11;
pub const SKELETON_ROW_HEIGHT: u32 = 20;
pub const SKELETON_ROW_GAP: u32 = 8;
pub const COLOR_WELL_SIZE: u32 = 60;
pub const COLOR_HISTORY_PER_ROW: u32 = 5;
pub const COLOR_HISTORY_SWATCH: u32 = 20;
pub const COLOR_HISTORY_PADDING: u32 = 4;
pub const COLOR_HISTORY_ROW_HEIGHT: u32 = COLOR_HISTORY_SWATCH;
pub const REFRESH_INDICATOR_HEIGHT: u32 = 56;
pub const DENSITY_STEP: u32 = 4;
pub const BREAKPOINT_COMPACT_MAX: u32 = 599;
pub const BREAKPOINT_MEDIUM_MAX: u32 = 839;
pub const CALENDAR_DEFAULT_WIDTH: u32 = 260;
pub const CALENDAR_DEFAULT_HEIGHT: u32 = 240;
pub const DATE_EDIT_DEFAULT_WIDTH: u32 = 120;
pub const DATE_EDIT_DEFAULT_HEIGHT: u32 = 28;
pub const PIE_MENU_DEFAULT_WIDTH: u32 = 200;
pub const PIE_MENU_DEFAULT_HEIGHT: u32 = 200;
pub const FRAME_DEFAULT_WIDTH: u32 = 200;
pub const FRAME_DEFAULT_HEIGHT: u32 = 200;
pub const PANEL_DEFAULT_WIDTH: u32 = 400;
pub const PANEL_DEFAULT_HEIGHT: u32 = 300;
pub const COLLAPSIBLE_PANE_DEFAULT_WIDTH: u32 = 200;
pub const COLLAPSIBLE_PANE_DEFAULT_HEIGHT: u32 = 100;
pub const DOCK_WIDGET_DEFAULT_WIDTH: u32 = 250;
pub const DOCK_WIDGET_DEFAULT_HEIGHT: u32 = 200;
pub const MASONRY_DEFAULT_WIDTH: u32 = 300;
pub const MASONRY_DEFAULT_HEIGHT: u32 = 300;
pub const SCROLL_AREA_DEFAULT_WIDTH: u32 = 300;
pub const SCROLL_AREA_DEFAULT_HEIGHT: u32 = 200;
pub const CUPERTINO_BAR_DEFAULT_WIDTH: u32 = 300;
pub const CUPERTINO_BAR_DEFAULT_HEIGHT: u32 = 48;
pub const CUPERTINO_PANEL_DEFAULT_WIDTH: u32 = 270;
pub const CUPERTINO_PANEL_DEFAULT_HEIGHT: u32 = 150;
pub const CUPERTINO_ROW_DEFAULT_WIDTH: u32 = 200;
pub const CUPERTINO_ROW_DEFAULT_HEIGHT: u32 = 28;
pub const CUPERTINO_TAB_BAR_DEFAULT_WIDTH: u32 = 72;
pub const CUPERTINO_TAB_BAR_DEFAULT_HEIGHT: u32 = 400;
pub const CUPERTINO_SEGMENTED_DEFAULT_WIDTH: u32 = 300;
pub const BOTTOM_SHEET_DEFAULT_WIDTH: u32 = 300;
pub const BOTTOM_SHEET_DEFAULT_HEIGHT: u32 = 200;
pub const DIALOG_WIDGET_DEFAULT_WIDTH: u32 = 320;
pub const FIND_REPLACE_DIALOG_DEFAULT_WIDTH: u32 = 350;
pub const FIND_REPLACE_DIALOG_DEFAULT_HEIGHT: u32 = 200;
pub const MESSAGE_BOX_DEFAULT_WIDTH: u32 = 350;
pub const MESSAGE_BOX_DEFAULT_HEIGHT: u32 = 150;
pub const POPOVER_DEFAULT_WIDTH: u32 = 200;
pub const POPOVER_DEFAULT_HEIGHT: u32 = 150;
pub const TOOLTIP_DEFAULT_WIDTH: u32 = 100;
pub const FLOATING_LABEL_DEFAULT_WIDTH: u32 = 200;
pub const FLOATING_LABEL_DEFAULT_HEIGHT: u32 = 40;
pub const FONT_PREVIEW_DEFAULT_WIDTH: u32 = 300;
pub const FONT_PREVIEW_DEFAULT_HEIGHT: u32 = 100;
pub const LCD_NUMBER_DEFAULT_WIDTH: u32 = 80;
pub const LCD_NUMBER_DEFAULT_HEIGHT: u32 = 30;
pub const PROGRESS_CIRCLE_DEFAULT_DIAMETER: u32 = 60;
pub const BAR_DEFAULT_EXTENT: u32 = 120;
pub const BAR_DEFAULT_THICKNESS: u32 = 20;
pub const SCROLLBAR_DEFAULT_LENGTH: u32 = 100;
pub const SKELETON_DEFAULT_WIDTH: u32 = 300;
pub const CASCADER_DEFAULT_WIDTH: u32 = 200;
pub const CASCADER_DEFAULT_HEIGHT: u32 = 32;
pub const INPLACE_EDITOR_DEFAULT_WIDTH: u32 = 200;
pub const INPLACE_EDITOR_DEFAULT_HEIGHT: u32 = 28;
pub const KEYBOARD_DEFAULT_WIDTH: u32 = 320;
pub const KEYBOARD_DEFAULT_HEIGHT: u32 = 160;
pub const RANGE_SLIDER_DEFAULT_WIDTH: u32 = 200;
pub const RANGE_SLIDER_DEFAULT_HEIGHT: u32 = 28;
pub const TEXT_EDIT_DEFAULT_WIDTH: u32 = 200;
pub const TEXT_EDIT_DEFAULT_HEIGHT: u32 = 24;
pub const AUDIO_VISUALIZER_DEFAULT_WIDTH: u32 = 200;
pub const AUDIO_VISUALIZER_DEFAULT_HEIGHT: u32 = 60;
pub const TOOL_BUTTON_SIZE: u32 = 28;
pub const TOOL_BUTTON_PADDING: u32 = 4;
pub const TOOL_BUTTON_ICON_SIZE: u32 = 16;
pub const TOOL_BUTTON_ICON_SPACING: u32 = 4;
pub const TOOL_BUTTON_POPUP_ARROW_RESERVE: u32 = 12;
pub const BEZIER_EDITOR_DEFAULT_WIDTH: u32 = 300;
pub const BEZIER_EDITOR_DEFAULT_HEIGHT: u32 = 200;
pub const QR_CODE_SIZE: u32 = 150;
pub const NAVIGATION_DRAWER_DEFAULT_WIDTH: u32 = 300;
pub const NAVIGATION_DRAWER_DEFAULT_HEIGHT: u32 = 400;
pub const REFRESH_CONTROL_DEFAULT_WIDTH: u32 = 400;
pub const REFRESH_CONTROL_DEFAULT_HEIGHT: u32 = 400;
pub const SPLASH_SCREEN_DEFAULT_WIDTH: u32 = 480;
pub const SPLASH_SCREEN_DEFAULT_HEIGHT: u32 = 320;
pub const SWIPE_TO_DISMISS_DEFAULT_WIDTH: u32 = 400;
pub const SWIPE_TO_DISMISS_DEFAULT_HEIGHT: u32 = 60;
pub const CHART_DEFAULT_WIDTH: u32 = 400;
pub const CHART_DEFAULT_HEIGHT: u32 = 300;
pub const GANTT_DEFAULT_WIDTH: u32 = 600;
pub const GANTT_DEFAULT_HEIGHT: u32 = 200;
pub const GRID_CELL_SIZE: u32 = 20;
pub const GRID_DEFAULT_FLOOR: u32 = 40;
pub const RADAR_CHART_DEFAULT_SIZE: u32 = 320;
pub const SEGMENTED_CONTROL_DEFAULT_WIDTH: u32 = 300;
pub const TERMINAL_DEFAULT_WIDTH: u32 = 600;
pub const TERMINAL_DEFAULT_HEIGHT: u32 = 300;
pub const DATA_GRID_DEFAULT_WIDTH: u32 = 400;
pub const DATA_GRID_DEFAULT_HEIGHT: u32 = 300;
pub const IMAGE_GALLERY_DEFAULT_WIDTH: u32 = 400;
pub const IMAGE_GALLERY_DEFAULT_HEIGHT: u32 = 300;
pub const LIST_VIEW_DEFAULT_WIDTH: u32 = 200;
pub const LIST_VIEW_DEFAULT_HEIGHT: u32 = 200;
pub const TABLE_WIDGET_DEFAULT_WIDTH: u32 = 400;
pub const TABLE_WIDGET_DEFAULT_HEIGHT: u32 = 300;
pub const TREE_TABLE_DEFAULT_WIDTH: u32 = 400;
pub const TREE_TABLE_DEFAULT_HEIGHT: u32 = 300;
pub const TREE_VIEW_DEFAULT_WIDTH: u32 = 200;
pub const TREE_VIEW_DEFAULT_HEIGHT: u32 = 200;
pub const VIRTUAL_LIST_DEFAULT_WIDTH: u32 = 200;
pub const VIRTUAL_LIST_DEFAULT_HEIGHT: u32 = 200;
pub const SPLIT_BUTTON_DEFAULT_WIDTH: u32 = 100;
pub const DISABLED_VEIL_ALPHA: u8 = 140;
pub fn density_scale(vertical_density: i32) -> i32 {
vertical_density.saturating_mul(DENSITY_STEP as i32)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SwitchGeometry {
pub track: Size,
pub thumb_radius: u32,
pub thumb_inset: u32,
pub press_stretch: u32,
}
impl SwitchGeometry {
pub const MATERIAL: Self = Self {
track: dimensions::SWITCH_TRACK,
thumb_radius: dimensions::SWITCH_THUMB_RADIUS,
thumb_inset: dimensions::SWITCH_THUMB_INSET,
press_stretch: 0,
};
pub const CUPERTINO: Self = Self {
track: dimensions::CUPERTINO_SWITCH_TRACK,
thumb_radius: dimensions::CUPERTINO_SWITCH_THUMB_RADIUS,
thumb_inset: dimensions::SWITCH_THUMB_INSET,
press_stretch: dimensions::CUPERTINO_SWITCH_PRESS_STRETCH,
};
pub const fn thumb_size(&self) -> u32 {
self.thumb_radius * 2
}
}
impl Default for SwitchGeometry {
fn default() -> Self {
Self::MATERIAL
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(all(feature = "serde", widgets_unstripped), derive(Serialize, Deserialize))]
#[cfg_attr(all(feature = "serde", widgets_unstripped), serde(rename_all = "lowercase"))]
pub enum ControlMetricsProfile {
#[default]
Material,
Macos,
Cupertino,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ProfileMetrics {
pub button_min: Size,
pub button_radius: u32,
pub switch_track: Size,
pub text_field_min_height: u32,
}
pub const fn profile_metrics(profile: ControlMetricsProfile) -> ProfileMetrics {
match profile {
ControlMetricsProfile::Material => ProfileMetrics {
button_min: dimensions::BUTTON_MIN,
button_radius: dimensions::BUTTON_RADIUS,
switch_track: dimensions::SWITCH_TRACK,
text_field_min_height: dimensions::TEXT_FIELD_MIN_HEIGHT,
},
ControlMetricsProfile::Macos => ProfileMetrics {
button_min: Size { width: dimensions::BUTTON_MIN.width, height: 28 },
button_radius: 6,
switch_track: Size { width: 38, height: 22 },
text_field_min_height: 22,
},
ControlMetricsProfile::Cupertino => ProfileMetrics {
button_min: dimensions::BUTTON_MIN,
button_radius: dimensions::BUTTON_RADIUS,
switch_track: dimensions::CUPERTINO_SWITCH_TRACK,
text_field_min_height: dimensions::TEXT_FIELD_MIN_HEIGHT,
},
}
}
pub fn active_profile() -> ControlMetricsProfile {
#[cfg(device_profile)]
let profile = crate::style::theme_manager()
.current_theme()
.map(|theme| theme.metrics_profile)
.unwrap_or_default();
#[cfg(not(device_profile))]
let profile = ControlMetricsProfile::Material;
profile
}
#[cfg(all(test, full_widgets))]
mod tests {
use super::*;
use crate::style::EdgeOffsets;
#[test]
fn small_content_still_gets_the_floor() {
let size = ControlMetrics::implicit_size(
Size::new(5, 5),
EdgeOffsets::symmetric(8, 12),
dimensions::BUTTON_MIN,
);
assert_eq!(size, dimensions::BUTTON_MIN);
}
#[test]
fn large_content_wins_over_the_floor() {
let size = ControlMetrics::implicit_size(
Size::new(200, 60),
EdgeOffsets::symmetric(8, 12),
dimensions::BUTTON_MIN,
);
assert_eq!(size, Size::new(224, 76));
}
#[test]
fn the_two_arms_are_compared_component_wise() {
let size = ControlMetrics::implicit_size(
Size::new(10, 90),
EdgeOffsets::symmetric(8, 12),
dimensions::BUTTON_MIN,
);
assert_eq!(size, Size::new(64, 106));
}
#[test]
fn content_box_removes_padding_from_both_sides() {
let rect = Rect::new(10, 20, 100, 50);
let content = ControlMetrics::content_box(rect, EdgeOffsets::symmetric(4, 6));
assert_eq!(content, Rect::new(16, 24, 88, 42));
}
#[test]
fn a_panel_is_capped_at_its_intrinsic_size_and_centred() {
let cell = crate::widget::census::CENSUS_RECT;
let panel = ControlMetrics::painted_box(
cell,
Size::new(dimensions::DIALOG_MIN_WIDTH, dimensions::DIALOG_MIN_HEIGHT),
);
assert_eq!(panel, Rect::new(0, 0, 240, 120));
let roomy = Rect::new(0, 0, 400, 300);
let centred = ControlMetrics::painted_box(
roomy,
Size::new(dimensions::DIALOG_MIN_WIDTH, dimensions::DIALOG_MIN_HEIGHT),
);
assert_eq!(centred, Rect::new(60, 30, 280, 240));
}
#[test]
fn a_panel_never_collapses_to_zero_extent() {
let degenerate = Rect::new(5, 5, 0, 0);
let panel = ControlMetrics::painted_box(degenerate, Size::new(280, 240));
assert_eq!(panel.width, 1);
assert_eq!(panel.height, 1);
}
#[test]
fn oversized_padding_collapses_the_content_box_instead_of_inverting_it() {
let rect = Rect::new(0, 0, 10, 10);
let content = ControlMetrics::content_box(rect, EdgeOffsets::all(50));
assert_eq!(content.width, 0);
assert_eq!(content.height, 0);
assert_eq!(content.x, 50);
assert_eq!(content.y, 50);
}
#[test]
fn centered_chrome_is_centred_and_clamped_never_expanded() {
let roomy = ControlMetrics::center_in(Rect::new(0, 0, 240, 120), dimensions::SWITCH_TRACK);
assert_eq!(roomy, Rect::new(94, 44, 52, 32));
let tight = ControlMetrics::center_in(Rect::new(5, 7, 20, 10), dimensions::SWITCH_TRACK);
assert_eq!(tight, Rect::new(5, 7, 20, 10));
}
#[test]
fn a_disc_stays_square_in_a_non_square_area() {
let disc = ControlMetrics::centered_disc(Rect::new(0, 0, 100, 20), 18);
assert_eq!(disc.width, disc.height);
assert_eq!(disc, Rect::new(41, 1, 18, 18));
}
#[test]
fn a_leading_box_sits_at_the_padding_edge_and_never_leaves_the_rect() {
let indicator = ControlMetrics::leading_box(
Rect::new(0, 0, 100, 40),
Size::new(dimensions::CHECKBOX_BOX, dimensions::CHECKBOX_BOX),
EdgeOffsets::all(2),
);
assert_eq!(indicator, Rect::new(2, 11, 18, 18));
let pinned = ControlMetrics::leading_box(
Rect::new(0, 0, 20, 40),
Size::new(dimensions::CHECKBOX_BOX, dimensions::CHECKBOX_BOX),
EdgeOffsets::all(50),
);
assert_eq!(pinned.x, 2);
assert_eq!(pinned.x + pinned.width as i32, 20);
}
#[test]
fn a_band_is_centred_and_keeps_its_thickness() {
let band =
ControlMetrics::centered_band(Rect::new(0, 0, 240, 120), dimensions::PROGRESS_HEIGHT);
assert_eq!(band, Rect::new(0, 58, 240, 4));
let clamped = ControlMetrics::centered_band(Rect::new(0, 10, 100, 2), 4);
assert_eq!(clamped, Rect::new(0, 10, 100, 2));
}
#[test]
fn the_dimensions_table_is_internally_consistent() {
const {
assert!(dimensions::SLIDER_THUMB_RADIUS * 2 > dimensions::SLIDER_TRACK_HEIGHT);
assert!(dimensions::SLIDER_TRACK_RADIUS * 2 <= dimensions::SLIDER_TRACK_HEIGHT);
assert!(
dimensions::SWITCH_THUMB_RADIUS * 2 + dimensions::SWITCH_THUMB_INSET * 2
== dimensions::SWITCH_TRACK.height
);
assert!(dimensions::SWITCH_THUMB_RADIUS * 2 < dimensions::SWITCH_TRACK.width);
assert!(dimensions::PROGRESS_RADIUS * 2 == dimensions::PROGRESS_HEIGHT);
assert!(dimensions::PROGRESS_HEIGHT <= dimensions::SLIDER_TRACK_HEIGHT * 2);
assert!(dimensions::SCROLLBAR_MIN_LENGTH > dimensions::SCROLLBAR_THICKNESS);
assert!(dimensions::CHECKBOX_BOX >= dimensions::RADIO_OUTER_RADIUS * 2);
assert!(dimensions::CHECKBOX_BOX - dimensions::RADIO_OUTER_RADIUS * 2 <= 2);
assert!(dimensions::RADIO_DOT_RADIUS < dimensions::RADIO_OUTER_RADIUS);
assert!(dimensions::MENU_BAR_HEIGHT < dimensions::TOOLBAR_HEIGHT);
assert!(dimensions::STATUS_BAR_HEIGHT <= dimensions::TOOLBAR_HEIGHT);
assert!(dimensions::TAB_HEIGHT <= dimensions::TOOLBAR_HEIGHT);
assert!(dimensions::PAGINATION_HEIGHT <= dimensions::TOOLBAR_HEIGHT);
assert!(dimensions::BREADCRUMB_HEIGHT <= dimensions::TOOLBAR_HEIGHT);
assert!(dimensions::STEPPER_BUTTON_WIDTH <= dimensions::STEPPER_ROW_HEIGHT);
assert!(dimensions::STEPPER_ROW_HEIGHT - dimensions::STEPPER_PADDING * 2 > 0);
assert!(dimensions::TEXT_FIELD_MIN_HEIGHT >= dimensions::TOUCH_TARGET_MIN);
assert!(dimensions::TOUCH_TARGET_MIN > dimensions::BUTTON_MIN.height);
assert!(dimensions::SEARCH_BOX_FIELD_HEIGHT == dimensions::TEXT_FIELD_MIN_HEIGHT);
assert!(dimensions::BADGE_PILL_HEIGHT > dimensions::BADGE_LABEL_FONT_SIZE);
assert!(dimensions::COLOR_HISTORY_ROW_HEIGHT == dimensions::COLOR_HISTORY_SWATCH);
assert!(dimensions::COLOR_HISTORY_PER_ROW > 0);
assert!(dimensions::SKELETON_ROW_GAP < dimensions::SKELETON_ROW_HEIGHT);
assert!(dimensions::REFRESH_INDICATOR_HEIGHT < 120);
assert!(dimensions::ROLLER_VISIBLE_ROWS == dimensions::PICKER_VISIBLE_ROWS);
assert!(
dimensions::ROLLER_WHEEL_HEIGHT
== dimensions::ROLLER_VISIBLE_ROWS * dimensions::ROLLER_ROW_HEIGHT
);
}
}
#[test]
fn mirrored_offsets_exchange_horizontal_sides_only() {
let offsets = EdgeOffsets::new(1, 2, 3, 4);
let mirrored = offsets.mirrored();
assert_eq!(mirrored.top, 1);
assert_eq!(mirrored.bottom, 3);
assert_eq!(mirrored.left, 2);
assert_eq!(mirrored.right, 4);
assert_eq!(mirrored.horizontal_total(), offsets.horizontal_total());
}
#[test]
fn a_focus_ring_is_inset_so_it_never_leaves_the_control() {
let ring = ControlMetrics::focus_ring_rect(Rect::new(0, 0, 64, 40));
assert_eq!(ring, Rect::new(2, 2, 60, 36));
assert!(ring.x >= 0 && ring.y >= 0);
assert!(ring.x + ring.width as i32 <= 64);
assert!(ring.y + ring.height as i32 <= 40);
let tiny = ControlMetrics::focus_ring_rect(Rect::new(5, 5, 2, 2));
assert_eq!(tiny.width, 0);
assert_eq!(tiny.height, 0);
}
#[test]
fn a_focus_ring_follows_the_control_corner_tightly() {
assert_eq!(ControlMetrics::focus_ring_radius(0), 0);
assert_eq!(ControlMetrics::focus_ring_radius(6), 4);
assert_eq!(ControlMetrics::focus_ring_radius(1), 0);
}
#[test]
fn sub_control_rects_tile_the_control_without_overlap() {
let rect = Rect::new(10, 20, 200, 40);
let metrics = SubControlMetrics::default().with_leading(16).with_trailing(24).with_gap(4);
let leading = ControlMetrics::sub_control_rect(rect, SubControl::Leading, metrics);
assert_eq!(leading, Rect::new(10, 20, 16, 40));
let contents = ControlMetrics::sub_control_rect(rect, SubControl::Contents, metrics);
assert_eq!(
contents,
Rect::new(30, 20, 200 - 16 - 4 - 24 - 4, 40),
"content is inset by both columns and their gaps"
);
let trailing = ControlMetrics::sub_control_rect(rect, SubControl::Trailing, metrics);
assert_eq!(trailing, Rect::new(210 - 24, 20, 24, 40));
assert_eq!(contents.x + contents.width as i32 + 4, trailing.x, "one gap between them");
let increment = ControlMetrics::sub_control_rect(rect, SubControl::Increment, metrics);
let decrement = ControlMetrics::sub_control_rect(rect, SubControl::Decrement, metrics);
assert_eq!(increment.y, rect.y, "the increment is the upper half");
assert_eq!(increment.height + decrement.height, rect.height, "the halves meet exactly");
assert_eq!(decrement.y, increment.y + increment.height as i32);
assert_eq!(increment.x, trailing.x);
assert_eq!(decrement.width, trailing.width);
let tiny = Rect::new(0, 0, 10, 8);
let contents = ControlMetrics::sub_control_rect(tiny, SubControl::Contents, metrics);
assert_eq!(contents.width, 0, "no negative width when the columns consume the box");
let trailing = ControlMetrics::sub_control_rect(tiny, SubControl::Trailing, metrics);
assert!(trailing.width <= tiny.width && trailing.x >= tiny.x);
let plain =
ControlMetrics::sub_control_rect(rect, SubControl::Contents, SubControlMetrics::NONE);
assert_eq!(plain, rect);
}
#[test]
fn a_close_affordance_is_a_centred_square() {
let rect = Rect::new(0, 0, 100, 40);
let metrics = SubControlMetrics::default().with_close(16);
let close = ControlMetrics::sub_control_rect(rect, SubControl::Close, metrics);
assert_eq!(close, Rect::new(84, 12, 16, 16));
let clipped = ControlMetrics::sub_control_rect(
Rect::new(0, 0, 10, 6),
SubControl::Close,
SubControlMetrics::default().with_close(16),
);
assert_eq!(clipped, Rect::new(4, 0, 6, 6));
}
#[test]
fn horizontal_and_vertical_totals_add_both_sides() {
let offsets = EdgeOffsets::new(3, 5, 7, 11);
assert_eq!(offsets.horizontal_total(), 16);
assert_eq!(offsets.vertical_total(), 10);
}
#[test]
fn a_top_band_keeps_its_thickness_at_the_top_edge() {
let band = ControlMetrics::top_band(Rect::new(0, 0, 240, 120), dimensions::TAB_HEIGHT);
assert_eq!(band, Rect::new(0, 0, 240, 24));
let clamped = ControlMetrics::top_band(Rect::new(5, 7, 100, 10), 40);
assert_eq!(clamped, Rect::new(5, 7, 100, 10));
}
#[test]
fn a_bottom_band_keeps_its_thickness_at_the_bottom_edge() {
let band = ControlMetrics::bottom_band(Rect::new(0, 0, 240, 120), dimensions::TAB_HEIGHT);
assert_eq!(band, Rect::new(0, 96, 240, 24));
assert_eq!(band.y + band.height as i32, 120, "the strip must end on the control's edge");
}
#[test]
fn a_bands_inset_is_measured_from_the_band_not_the_control() {
let strip = ControlMetrics::top_band(Rect::new(0, 0, 240, 120), dimensions::TOOLBAR_HEIGHT);
let item = ControlMetrics::band_inset(strip, dimensions::TOOLBAR_ITEM_INSET);
assert_eq!(item, Rect::new(2, 2, 236, 52));
let collapsed = ControlMetrics::band_inset(Rect::new(4, 4, 6, 6), 10);
assert_eq!(collapsed.width, 0);
assert_eq!(collapsed.height, 0);
}
#[test]
fn a_top_band_and_the_content_below_it_tile_the_rectangle() {
let rect = Rect::new(0, 0, 240, 120);
let band = ControlMetrics::top_band(rect, dimensions::TAB_HEIGHT);
let content = ControlMetrics::content_below_top_band(rect, dimensions::TAB_HEIGHT);
assert_eq!(content.y, band.y + band.height as i32);
assert_eq!(content.height + band.height, rect.height);
assert_eq!(content, Rect::new(0, 24, 240, 96));
}
#[test]
fn a_bottom_band_and_the_content_above_it_tile_the_rectangle() {
let rect = Rect::new(0, 0, 240, 120);
let band = ControlMetrics::bottom_band(rect, dimensions::TAB_HEIGHT);
let content = ControlMetrics::content_above_bottom_band(rect, dimensions::TAB_HEIGHT);
assert_eq!(content.y + content.height as i32, band.y);
assert_eq!(content.height + band.height, rect.height);
assert_eq!(content, Rect::new(0, 0, 240, 96));
}
#[test]
fn a_strip_taller_than_the_control_leaves_no_content_rather_than_a_negative_one() {
let rect = Rect::new(10, 20, 100, 10);
assert_eq!(ControlMetrics::content_below_top_band(rect, 40).height, 0);
assert_eq!(ControlMetrics::content_above_bottom_band(rect, 40).height, 0);
}
#[test]
fn the_text_estimate_reproduces_the_renderers_advance_model() {
let font = crate::core::Font::simple("sans-serif", 14.0);
let renderer = crate::render::text::measure_text_width_for_test("abcd", &font, 1.0);
assert_eq!(
estimate_text_width("abcd", &font, 1.0),
renderer,
"a `size_hint` and the paint must use the same ruler"
);
assert_eq!(estimate_text_width("", &font, 1.0), 0);
assert!(
estimate_text_width("\u{4e2d}", &font, 1.0) > estimate_text_width("a", &font, 1.0),
"a wide scalar must advance more than a narrow one"
);
assert_eq!(estimate_line_height(&font, 1.0), 14);
}
#[test]
fn the_fallback_advance_model_matches_the_renderers_fallback() {
let font = crate::core::Font::simple("sans-serif", 14.0);
let exotic = "\u{e000}\u{e001}";
let mut expected = 0.0f32;
crate::render::text::for_each_cluster(exotic, |cluster, _range| {
expected += estimate_cluster_advance(cluster, 14.0, 1.0);
});
assert_eq!(estimate_text_width(exotic, &font, 1.0), expected.round().max(0.0) as u32);
}
#[test]
fn letter_spacing_is_paid_between_clusters_and_not_after_the_last() {
let plain = crate::core::Font::simple("sans-serif", 14.0);
let mut tracked = crate::core::Font::simple("sans-serif", 14.0);
tracked.set_letter_spacing(4.0);
let one = estimate_text_width("a", &tracked, 1.0);
assert_eq!(
one,
estimate_text_width("a", &plain, 1.0),
"a single cluster has no gap to pay tracking on"
);
assert_eq!(
estimate_text_width("abc", &tracked, 1.0),
estimate_text_width("abc", &plain, 1.0) + 8
);
}
#[test]
fn the_shared_cluster_buffer_does_not_leak_between_measurements() {
let font = crate::core::Font::simple("sans-serif", 14.0);
let long = estimate_text_width("Hello, world", &font, 1.0);
let short = estimate_text_width("i", &font, 1.0);
assert!(long > short, "the fixture must actually differ in width: {long} vs {short}");
assert_eq!(estimate_text_width("Hello, world", &font, 1.0), long);
assert_eq!(estimate_text_width("i", &font, 1.0), short);
assert_eq!(estimate_text_width("", &font, 1.0), 0);
assert_eq!(estimate_text_width("Hello, world", &font, 1.0), long);
assert_eq!(estimate_text_width("i", &font, 1.0), short);
}
#[test]
fn the_line_box_follows_an_explicit_line_height() {
let font = crate::core::Font::simple("sans-serif", 10.0);
assert_eq!(estimate_line_height(&font, 1.0), 10);
let mut taller = font.clone();
taller.set_line_height(20.0);
assert_eq!(estimate_line_height(&taller, 1.0), 20);
}
#[test]
fn minimum_hit_size_matches_the_platform_profile() {
assert_eq!(
minimum_hit_size(),
crate::platform::profile::recommended_touch_target().dimensions(),
"the hit-area floor must come from the device class the platform reports"
);
}
#[test]
fn expand_to_hit_area_expands_both_axes_and_is_idempotent() {
let floor = minimum_hit_size();
let small = Rect::new(10, 20, 4, 6);
assert!(
small.width < floor.width && small.height < floor.height,
"the fixture must be below the floor on both axes: {small:?} vs {floor:?}"
);
let expanded = expand_to_hit_area(small);
assert_eq!(expanded.width, floor.width, "a narrow region reaches the width floor");
assert_eq!(expanded.height, floor.height, "a short region reaches the height floor");
assert_eq!(
expanded.x,
small.x - (expanded.width - small.width) as i32 / 2,
"the expansion is centred horizontally"
);
assert_eq!(
expanded.y,
small.y - (expanded.height - small.height) as i32 / 2,
"the expansion is centred vertically"
);
assert_eq!(expand_to_hit_area(expanded), expanded, "a region at the floor must not move");
let large = Rect::new(5, 6, 200, 100);
assert_eq!(expand_to_hit_area(large), large);
}
#[test]
fn focus_ring_color_uses_outline_when_the_theme_provides_one_and_falls_back_otherwise() {
let fallback = Color::rgb(200, 210, 220);
let outline = Color::rgb(1, 2, 3);
assert_eq!(ring_color_from(Some(outline), fallback), outline);
assert_eq!(ring_color_from(None, fallback), fallback.contrast_color());
assert_ne!(
outline,
fallback.contrast_color(),
"the fixture must distinguish the two branches for this assertion to mean anything"
);
}
#[cfg(device_profile)]
#[test]
fn focus_ring_color_reads_the_active_themes_outline_token() {
use crate::compat::ToString;
let _guard = crate::style::theme_test_guard();
let mut theme = crate::theme::Theme::default();
theme.name = "p1-5-outline".to_string();
theme.colors.outline = Color::rgb(7, 8, 9);
{
let mut manager = crate::theme::global_theme_manager();
manager.register_theme(theme);
assert!(manager.set_theme("p1-5-outline"), "the fixture theme must become active");
}
assert_eq!(focus_ring_color(Color::WHITE), Color::rgb(7, 8, 9));
assert!(crate::theme::global_theme_manager().set_theme("default"));
}
#[test]
fn a_halo_ring_sits_one_ring_width_outside_the_control() {
let rect = Rect::new(10, 20, 40, 30);
let ring = FocusRing::halo(rect, 6);
let width = FOCUS_RING_WIDTH as i32;
assert_eq!(
ring.rect,
Rect::new(
rect.x - width,
rect.y - width,
rect.width + 2 * FOCUS_RING_WIDTH,
rect.height + 2 * FOCUS_RING_WIDTH,
)
);
assert_eq!(ring.radius, 6 + FOCUS_RING_WIDTH, "the radius follows the grown rectangle");
assert!(ring.is_drawable(), "the halo has area to paint");
}
#[test]
fn a_macos_halo_grows_by_the_halo_width() {
let rect = Rect::new(10, 20, 40, 30);
let ring = FocusRing::halo_for_control(rect, 6);
let grow = FOCUS_HALO_WIDTH as i32;
assert_eq!(
ring.rect,
Rect::new(
rect.x - grow,
rect.y - grow,
rect.width + 2 * FOCUS_HALO_WIDTH,
rect.height + 2 * FOCUS_HALO_WIDTH,
)
);
assert_eq!(ring.radius, 6 + FOCUS_HALO_WIDTH);
assert!(
ring.rect.width > FocusRing::halo(rect, 6).rect.width,
"the macOS halo must be wider than the inset-inverse halo"
);
}
#[test]
fn the_material_profile_keeps_the_existing_constants() {
let metrics = profile_metrics(ControlMetricsProfile::Material);
assert_eq!(metrics.button_min, dimensions::BUTTON_MIN);
assert_eq!(metrics.button_radius, dimensions::BUTTON_RADIUS);
assert_eq!(metrics.switch_track, dimensions::SWITCH_TRACK);
assert_eq!(metrics.text_field_min_height, dimensions::TEXT_FIELD_MIN_HEIGHT);
}
#[test]
fn the_macos_profile_matches_the_appkit_metrics() {
let metrics = profile_metrics(ControlMetricsProfile::Macos);
assert_eq!(metrics.button_min.height, 28, "AppKit's push button is 28 px tall");
assert_eq!(metrics.button_radius, 6);
assert_eq!(metrics.switch_track, Size { width: 38, height: 22 }, "NSSwitch is 38x22");
assert_eq!(metrics.text_field_min_height, 22);
}
#[test]
fn the_cupertino_profile_uses_the_cupertino_switch_track() {
let metrics = profile_metrics(ControlMetricsProfile::Cupertino);
assert_eq!(metrics.switch_track, dimensions::CUPERTINO_SWITCH_TRACK);
assert_eq!(metrics.button_min, dimensions::BUTTON_MIN);
}
#[test]
fn the_default_metrics_profile_is_material() {
assert_eq!(ControlMetricsProfile::default(), ControlMetricsProfile::Material);
}
#[test]
fn focus_ring_color_from_theme_reads_the_outline_token() {
let mut colors = crate::style::Colors::default();
colors.outline = Color::rgb(11, 22, 33);
assert_eq!(focus_ring_color_from_theme(&colors), Color::rgb(11, 22, 33));
}
#[cfg(device_profile)]
#[test]
fn active_profile_reads_the_active_theme() {
use crate::compat::ToString;
let _guard = crate::style::theme_test_guard();
let mut theme = crate::theme::Theme::default();
theme.name = "p2-6-macos".to_string();
theme.metrics_profile = ControlMetricsProfile::Macos;
{
let mut manager = crate::theme::global_theme_manager();
manager.register_theme(theme);
assert!(manager.set_theme("p2-6-macos"), "the fixture theme must become active");
}
assert_eq!(active_profile(), ControlMetricsProfile::Macos);
assert!(crate::theme::global_theme_manager().set_theme("default"));
}
}