use crate::core::{Rect, Size};
use crate::render::text::{estimate_cluster_advance, for_each_cluster};
use crate::style::EdgeOffsets;
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;
}
let mut advance = 0.0f32;
let mut clusters = 0usize;
for_each_cluster(text, |cluster, _range| {
advance += estimate_cluster_advance(cluster, size, scale);
clusters += 1;
});
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 struct ControlMetrics;
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 const FOCUS_RING_WIDTH: u32 = 2;
#[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 focus_ring_color(fallback: crate::core::Color) -> crate::core::Color {
fallback.contrast_color()
}
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 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 = 40;
pub const DENSITY_STEP: u32 = 4;
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
}
}
#[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 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);
assert_eq!(estimate_text_width("abcd", &font, 1.0), 34);
assert_eq!(estimate_text_width("\u{4e2d}\u{6587}", &font, 1.0), 28);
assert_eq!(estimate_text_width(" ", &font, 1.0), 9);
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 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_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);
}
}