use crate::core::HorizontalAlignment;
use crate::core::Point;
use crate::core::{Color, Rect};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::{GenericSignal, Signal1};
use crate::widget::capability::coercion::{expect_f32, expect_string};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::display_widgets::switch::Switch;
use crate::widget::metrics::role_font;
use crate::widget::metrics::SwitchGeometry;
use crate::widget::numeric::ordered_clamp;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
pub use super::material::{MaterialNavigationRail, MaterialSnackbar, RailItem};
pub const CUPERTINO_ON_TRACK: Color = Color::rgb(52, 199, 89);
pub struct CupertinoSwitch(pub Switch);
impl CupertinoSwitch {
pub fn new(geometry: Rect) -> Self {
let mut sw = Switch::new(geometry);
sw.set_geometry(SwitchGeometry::CUPERTINO);
sw.set_on_track_color(Some(CUPERTINO_ON_TRACK));
Self(sw)
}
pub fn inner(&self) -> &Switch {
&self.0
}
pub fn inner_mut(&mut self) -> &mut Switch {
&mut self.0
}
}
impl Widget for CupertinoSwitch {
fn base(&self) -> &BaseWidget {
self.0.base()
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
self.0.base_mut()
}
fn size_hint(&self) -> crate::core::Size {
debug_assert!(
crate::widget::metrics::ControlMetrics::content_box(
crate::core::Rect::new(0, 0, 1, 1),
crate::style::EdgeOffsets::default(),
)
.width
== 1,
"a size hint must read the shared metric system"
);
self.0.size_hint()
}
fn kind(&self) -> WidgetKind {
WidgetKind::CupertinoSwitch
}
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
self.0.event_signal_dyn(name)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for CupertinoSwitch {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
self.0.get(name)
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
self.0.set(name, value)
}
fn property_names(&self) -> &'static [&'static str] {
self.0.property_names()
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
self.0.command(name)
}
}
impl Draw for CupertinoSwitch {
fn draw(&mut self, context: &mut RenderContext) {
self.0.draw(context);
}
}
impl EventHandler for CupertinoSwitch {
fn handle_event(&mut self, event: &Event) {
self.0.handle_event(event);
}
}
pub struct CupertinoAlertDialog {
base: BaseWidget,
title: String,
message: String,
confirm_text: String,
cancel_text: String,
pub confirmed: GenericSignal,
pub canceled: GenericSignal,
}
impl CupertinoAlertDialog {
fn panel_rect(&self) -> crate::core::Rect {
let rect = self.geometry();
let width = rect.width.min(320);
let height = rect.height.min(200);
crate::core::Rect::new(
rect.x + (rect.width as i32 - width as i32) / 2,
rect.y + (rect.height as i32 - height as i32) / 2,
width,
height,
)
}
fn button_row_rect(&self) -> crate::core::Rect {
let panel = self.panel_rect();
let row_height = (panel.height / 3).clamp(1, panel.height);
crate::core::Rect::new(
panel.x,
panel.y + panel.height as i32 - row_height as i32,
panel.width,
row_height,
)
}
pub fn new(geometry: crate::core::Rect) -> Self {
let base =
BaseWidget::new(WidgetKind::CupertinoAlertDialog, geometry, "CupertinoAlertDialog");
Self {
base,
title: String::new(),
message: String::new(),
confirm_text: "OK".to_string(),
cancel_text: "Cancel".to_string(),
confirmed: GenericSignal::new(),
canceled: GenericSignal::new(),
}
}
pub fn set_title(&mut self, title: &str) {
if self.title == title {
return;
}
self.title = title.to_string();
self.base.request_redraw();
}
pub fn title(&self) -> &str {
&self.title
}
pub fn cancelled(&self) -> &GenericSignal {
&self.canceled
}
pub fn set_message(&mut self, message: &str) {
if self.message == message {
return;
}
self.message = message.to_string();
self.base.request_redraw();
}
pub fn message(&self) -> &str {
&self.message
}
pub fn set_confirm_text(&mut self, text: &str) {
if self.confirm_text == text {
return;
}
self.confirm_text = text.to_string();
self.base.request_redraw();
}
pub fn set_cancel_text(&mut self, text: &str) {
if self.cancel_text == text {
return;
}
self.cancel_text = text.to_string();
self.base.request_redraw();
}
}
impl Widget for CupertinoAlertDialog {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(
crate::widget::metrics::dimensions::CUPERTINO_PANEL_DEFAULT_WIDTH,
crate::widget::metrics::dimensions::CUPERTINO_PANEL_DEFAULT_HEIGHT,
)
}
fn kind(&self) -> WidgetKind {
WidgetKind::CupertinoAlertDialog
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"confirmed" => Some(EventSignalRef::unit("confirmed", &self.confirmed)),
"canceled" => Some(EventSignalRef::unit("canceled", &self.canceled)),
_ => None,
}
}
}
impl WidgetProperties for CupertinoAlertDialog {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"title" => Ok(CapabilityValue::String(self.title().to_string())),
"message" => Ok(CapabilityValue::String(self.message().to_string())),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"title" => {
self.set_title(&expect_string(value)?);
Ok(())
}
"message" => {
self.set_message(&expect_string(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["title", "message", BASE_PROPERTY_NAMES]
}
}
impl Draw for CupertinoAlertDialog {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let style = self.base.style();
let theme = crate::style::resolved_theme_style_shared("cupertino_alert_dialog");
let (window_fill, foreground, primary, background) = {
let manager = crate::style::theme_manager();
match manager.current_theme() {
Some(active) => (
active.colors.background,
active.colors.foreground,
active.colors.primary,
active.colors.background,
),
None => (
Color::rgb(240, 240, 240),
Color::BLACK,
Color::rgb(0, 122, 255),
Color::rgb(240, 240, 240),
),
}
};
let ink = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(foreground);
let panel_from_theme = window_fill.blend(&ink, 0.10);
let panel = match style.background_color {
Some(resolved) if resolved != window_fill => resolved,
_ => panel_from_theme,
};
let separator = theme
.as_ref()
.and_then(|t| t.border_color)
.filter(|resolved| *resolved != panel)
.unwrap_or_else(|| panel.blend(&ink, 0.22));
let action = style.text_color.map(|_| ink).unwrap_or_else(|| primary.contrast_color());
let scrim = panel.blend(&background, 0.62);
let dialog_rect = self.panel_rect();
let dialog_width = dialog_rect.width;
let dialog_height = dialog_rect.height;
context.fill_rect(rect, scrim);
if dialog_width == 0 || dialog_height == 0 {
return;
}
context.fill_rounded_rect(dialog_rect, 14, panel);
let title_font = role_font(style, 17.0).with_bold(true);
if !self.title.is_empty() {
let title_metrics = context.measure_text("M", &title_font);
let title_y = dialog_rect.y + ((dialog_height as i32 * 24) / 100).max(0);
context.draw_text_fitted_cow(
Rect::new(
dialog_rect.x + 12,
title_y,
dialog_width.saturating_sub(24),
title_metrics.height.max(1),
),
&self.title,
&title_font,
ink,
HorizontalAlignment::Center,
);
}
let msg_font = role_font(style, 14.0);
if !self.message.is_empty() {
let msg_metrics = context.measure_text("M", &msg_font);
let msg_y = dialog_rect.y + ((dialog_height as i32 * 46) / 100).max(0);
context.draw_text_fitted_cow(
Rect::new(
dialog_rect.x + 12,
msg_y,
dialog_width.saturating_sub(24),
msg_metrics.height.max(1),
),
&self.message,
&msg_font,
ink,
HorizontalAlignment::Center,
);
}
let button_row = self.button_row_rect();
let divider_y = button_row.y;
context.draw_line(
Point::new(dialog_rect.x, divider_y),
Point::new(dialog_rect.x + dialog_width as i32, divider_y),
separator,
);
let button_font = role_font(style, 17.0);
let button_metrics = context.measure_text("M", &button_font);
let has_cancel = !self.cancel_text.is_empty();
let button_top = button_row.y;
let button_bottom = button_row.y + button_row.height as i32;
let label_height = button_metrics.height.max(1) as i32;
let label_y =
(button_top + (button_bottom - button_top - label_height) / 2).max(button_top);
let cell_width = if has_cancel { dialog_width as i32 / 2 } else { dialog_width as i32 };
if has_cancel {
let mid_x = dialog_rect.x + dialog_width as i32 / 2;
context.draw_line(
Point::new(mid_x, divider_y),
Point::new(mid_x, button_bottom),
separator,
);
context.draw_text_fitted_cow(
Rect::new(dialog_rect.x, label_y, cell_width.max(0) as u32, label_height as u32),
&self.cancel_text,
&button_font,
action,
HorizontalAlignment::Center,
);
context.draw_text_fitted_cow(
Rect::new(mid_x, label_y, cell_width.max(0) as u32, label_height as u32),
&self.confirm_text,
&button_font,
action,
HorizontalAlignment::Center,
);
} else {
context.draw_text_fitted_cow(
Rect::new(dialog_rect.x, label_y, dialog_width, label_height as u32),
&self.confirm_text,
&button_font,
action,
HorizontalAlignment::Center,
);
}
}
}
impl EventHandler for CupertinoAlertDialog {
fn handle_event(&mut self, event: &Event) {
if !self.base.is_enabled() {
return;
}
match event {
Event::MouseRelease { pos, button } => {
if *button != 1 {
return;
}
let row = self.button_row_rect();
if !row.contains_point(*pos) {
return;
}
let has_cancel = !self.cancel_text.is_empty();
if has_cancel {
let mid_x = row.x + row.width as i32 / 2;
if pos.x < mid_x {
self.canceled.emit();
} else {
self.confirmed.emit();
}
} else {
self.confirmed.emit();
}
self.base.request_redraw();
}
Event::KeyPress { key, .. } => match *key {
13 | 10 => {
self.confirmed.emit();
self.base.request_redraw();
}
27 if !self.cancel_text.is_empty() => {
self.canceled.emit();
self.base.request_redraw();
}
_ => {
self.base.handle_event(event);
}
},
_ => {
self.base.handle_event(event);
}
}
}
}
pub struct CupertinoSlider {
base: BaseWidget,
value: f32,
min: f32,
max: f32,
dragging: bool,
pub value_changed: Signal1<f32>,
}
impl CupertinoSlider {
pub fn new(geometry: crate::core::Rect) -> Self {
let base = BaseWidget::new(WidgetKind::CupertinoSlider, geometry, "CupertinoSlider");
Self {
base,
value: 0.0,
min: 0.0,
max: 1.0,
dragging: false,
value_changed: Signal1::new(),
}
}
pub fn set_value(&mut self, value: f32) {
let clamped = ordered_clamp(value, self.min, self.max);
if self.value != clamped {
self.value = clamped;
self.value_changed.emit(clamped);
}
self.base.request_redraw();
}
pub fn value(&self) -> f32 {
self.value
}
pub fn set_min(&mut self, min: f32) {
self.min = min;
self.set_value(self.value);
}
pub fn set_max(&mut self, max: f32) {
self.max = max;
self.set_value(self.value);
}
pub fn min(&self) -> f32 {
self.min
}
pub fn max(&self) -> f32 {
self.max
}
fn knob_x(&self, track_left: i32, track_width: i32) -> i32 {
if self.max <= self.min {
return track_left;
}
let fraction = (self.value - self.min) / (self.max - self.min);
track_left + (fraction * track_width as f32) as i32
}
fn value_from_x(&self, x: i32, track_left: i32, track_width: i32) -> f32 {
if track_width <= 0 || self.max <= self.min {
return self.min;
}
let fraction = ((x - track_left) as f32 / track_width as f32).clamp(0.0, 1.0);
self.min + fraction * (self.max - self.min)
}
}
impl Widget for CupertinoSlider {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(
crate::widget::metrics::dimensions::CUPERTINO_ROW_DEFAULT_WIDTH,
crate::widget::metrics::dimensions::CUPERTINO_ROW_DEFAULT_HEIGHT,
)
}
fn kind(&self) -> WidgetKind {
WidgetKind::CupertinoSlider
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
#[allow(unused_imports)]
use crate::widget::capability::CapabilityValue;
match name {
"value_changed" => {
Some(EventSignalRef::mapped("value_changed", &self.value_changed, |v| {
CapabilityValue::Float(*v as f64)
}))
}
_ => None,
}
}
}
impl WidgetProperties for CupertinoSlider {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"value" => Ok(CapabilityValue::Float(self.value().into())),
"min" => Ok(CapabilityValue::Float(self.min().into())),
"max" => Ok(CapabilityValue::Float(self.max().into())),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"value" => {
self.set_value(expect_f32(value)?);
Ok(())
}
"min" => {
self.set_min(expect_f32(value)?);
Ok(())
}
"max" => {
self.set_max(expect_f32(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["value", "min", "max", BASE_PROPERTY_NAMES]
}
}
impl Draw for CupertinoSlider {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let track_height = 4u32;
let knob_radius = 12u32;
let track_y = rect.y + (rect.height as i32 / 2) - (track_height as i32 / 2);
let track_left = rect.x + knob_radius as i32;
let track_width = rect.width as i32 - 2 * knob_radius as i32;
if track_width <= 0 {
return;
}
let track_rect =
crate::core::Rect::new(track_left, track_y, track_width as u32, track_height);
let style = self.base.style();
let theme = crate::style::resolved_theme_style_shared("cupertino_slider");
let (window_fill, foreground, accent, muted) = {
let manager = crate::style::theme_manager();
match manager.current_theme() {
Some(active) => (
active.colors.background,
active.colors.foreground,
active.colors.accent,
active.colors.secondary,
),
None => (
Color::rgb(240, 240, 240),
Color::BLACK,
Color::rgb(33, 150, 243),
Color::rgb(158, 158, 158),
),
}
};
let ink = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(foreground);
let empty_track = window_fill.blend(&ink, 0.14);
let track_color = match style.background_color {
Some(resolved) if resolved != window_fill => resolved,
_ => empty_track,
};
context.fill_rounded_rect(track_rect, track_height / 2, track_color);
let knob_center_x = self.knob_x(track_left, track_width);
let fill_width = (knob_center_x - track_left) as u32;
if fill_width > 0 {
let fill_rect = crate::core::Rect::new(track_left, track_y, fill_width, track_height);
context.fill_rounded_rect(fill_rect, track_height / 2, accent);
}
let knob_fill = if track_color.is_dark() {
track_color.blend(&Color::WHITE, 0.30)
} else {
track_color.blend(&Color::WHITE, 0.85)
};
context.fill_circle_aa(
Point::new(knob_center_x, rect.y + (rect.height as i32 / 2)),
knob_radius,
knob_fill,
);
let knob_border = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.filter(|resolved| *resolved != knob_fill)
.unwrap_or_else(|| knob_fill.blend(&muted, 0.40));
context.draw_circle_stroke(
Point::new(knob_center_x, rect.y + (rect.height as i32 / 2)),
knob_radius,
knob_border,
1,
);
}
}
impl EventHandler for CupertinoSlider {
fn handle_event(&mut self, event: &Event) {
if !self.base.is_enabled() {
return;
}
match event {
Event::MousePress { pos, button, .. } => {
if *button != 1 {
return;
}
let rect = self.geometry();
let knob_radius = 12u32;
let track_left = rect.x + knob_radius as i32;
let track_width = rect.width as i32 - 2 * knob_radius as i32;
if track_width <= 0 {
return;
}
let new_value = self.value_from_x(pos.x, track_left, track_width);
let clamped = ordered_clamp(new_value, self.min, self.max);
self.dragging = true;
if (clamped - self.value).abs() > f32::EPSILON {
self.value = clamped;
self.value_changed.emit(clamped);
self.base.request_redraw();
}
}
Event::Drag { pos, .. } if self.dragging => {
let rect = self.geometry();
let knob_radius = 12u32;
let track_left = rect.x + knob_radius as i32;
let track_width = rect.width as i32 - 2 * knob_radius as i32;
if track_width <= 0 {
return;
}
let new_value = self.value_from_x(pos.x, track_left, track_width);
let clamped = ordered_clamp(new_value, self.min, self.max);
if (clamped - self.value).abs() > f32::EPSILON {
self.value = clamped;
self.value_changed.emit(clamped);
self.base.request_redraw();
}
}
Event::MouseRelease { .. } if self.dragging => {
self.dragging = false;
}
_ => {
self.base.handle_event(event);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Point;
use crate::widget::metrics::dimensions;
use crate::widget::svg::render_to_svg;
use crate::widget::{IconItemError, IconSource};
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
};
#[test]
fn cupertino_switch_new_defaults() {
let cs = CupertinoSwitch::new(Rect::new(0, 0, 60, 30));
assert_eq!(cs.inner().kind(), WidgetKind::Switch);
assert_eq!(cs.kind(), WidgetKind::CupertinoSwitch);
assert!(!cs.inner().is_checked());
}
#[test]
fn cupertino_switch_toggle_delegates() {
let mut cs = CupertinoSwitch::new(Rect::new(0, 0, 60, 30));
assert!(!cs.inner().is_checked());
cs.inner_mut().toggle();
assert!(cs.inner().is_checked());
}
#[test]
fn cupertino_switch_event_delegation() {
let mut cs = CupertinoSwitch::new(Rect::new(0, 0, 60, 30));
let p = Point::new(10, 10);
cs.handle_event(&Event::MousePress { pos: p, button: 1, modifiers: 0 });
cs.handle_event(&Event::MouseRelease { pos: p, button: 1 });
assert!(cs.inner().is_checked());
}
#[test]
fn cupertino_switch_svg_output() {
let mut cs = CupertinoSwitch::new(Rect::new(0, 0, 60, 30));
let svg = render_to_svg(&mut cs);
assert!(svg.starts_with("<svg"));
}
#[test]
fn cupertino_switch_carries_the_ios_geometry() {
let _theme_guard = crate::style::theme_test_guard();
let material = Switch::new(Rect::new(0, 0, 240, 120));
let mut cupertino = CupertinoSwitch::new(Rect::new(0, 0, 240, 120));
assert_eq!(cupertino.inner().drawn_shape(), SwitchGeometry::CUPERTINO);
assert_ne!(
cupertino.inner().drawn_shape(),
material.drawn_shape(),
"an iOS switch must not be drawn in the Material shape"
);
assert_eq!(cupertino.size_hint(), cupertino.inner().size_hint());
let rect = Rect::new(0, 0, 240, 120);
let thumb = cupertino.inner().thumb_rect(rect).expect("a roomy rect has a thumb");
assert_eq!(thumb.width, dimensions::CUPERTINO_SWITCH_THUMB_RADIUS * 2);
let off = render_to_svg(&mut cupertino);
assert!(
!off.contains("rgba(52,199,89"),
"an off iOS switch must not be painted in the on colour"
);
cupertino.inner_mut().set_checked(true);
while cupertino.inner_mut().tick(1000) {}
let on = render_to_svg(&mut cupertino);
assert!(
on.contains("rgba(52,199,89"),
"an on iOS switch must be painted in UISwitch.onTintColor"
);
}
#[test]
fn the_held_ios_thumb_stretches_sideways() {
use crate::style::WidgetState;
let rect = Rect::new(0, 0, 240, 120);
let mut cupertino = CupertinoSwitch::new(rect);
let resting = cupertino.inner().thumb_rect(rect).expect("a roomy rect has a thumb");
let centre =
Point::new(resting.x + resting.width as i32 / 2, resting.y + resting.height as i32 / 2);
cupertino.handle_event(&Event::MouseEnter { pos: centre });
cupertino.handle_event(&Event::MousePress { pos: centre, button: 1, modifiers: 0 });
assert_eq!(cupertino.widget_state(), WidgetState::Pressed);
let held = cupertino.inner().thumb_rect(rect).expect("a roomy rect has a thumb");
assert_eq!(
held.width,
resting.width + dimensions::CUPERTINO_SWITCH_PRESS_STRETCH * 2,
"a held thumb must widen by the documented stretch"
);
assert_eq!(held.height, resting.height, "the stretch is sideways only");
assert_eq!(
held.x + held.width as i32 / 2,
resting.x + resting.width as i32 / 2,
"a stretch must not move the thumb's centre"
);
cupertino.handle_event(&Event::MouseRelease { pos: centre, button: 1 });
let released = cupertino.inner().thumb_rect(rect).expect("a roomy rect has a thumb");
assert_eq!(released.width, resting.width, "releasing must restore the disc");
}
#[test]
fn material_snackbar_creation() {
let sb = MaterialSnackbar::new(Rect::new(0, 0, 375, 100));
assert_eq!(sb.kind(), WidgetKind::MaterialSnackbar);
assert!(!sb.is_visible());
assert!(sb.message().is_empty());
assert!(sb.action_text().is_empty());
}
#[test]
fn material_snackbar_message_accessors() {
let mut sb = MaterialSnackbar::new(Rect::new(0, 0, 375, 100));
sb.set_message("File saved");
assert_eq!(sb.message(), "File saved");
}
#[test]
fn material_snackbar_action_text_accessors() {
let mut sb = MaterialSnackbar::new(Rect::new(0, 0, 375, 100));
sb.set_action_text("Undo");
assert_eq!(sb.action_text(), "Undo");
}
#[test]
fn material_snackbar_show_hide() {
let mut sb = MaterialSnackbar::new(Rect::new(0, 0, 375, 100));
assert!(!sb.is_visible());
sb.show();
assert!(sb.is_visible());
sb.dismiss();
assert!(!sb.is_visible());
}
#[test]
fn material_snackbar_dismiss_emits_signal() {
let mut sb = MaterialSnackbar::new(Rect::new(0, 0, 375, 100));
sb.set_message("Hello");
sb.show();
let fired = Arc::new(AtomicBool::new(false));
let f = fired.clone();
sb.dismissed.connect(move || {
f.store(true, Ordering::SeqCst);
});
sb.handle_event(&Event::MousePress { pos: Point::new(200, 80), button: 1, modifiers: 0 });
assert!(fired.load(Ordering::SeqCst));
assert!(!sb.is_visible());
}
#[test]
fn material_snackbar_action_pressed_emits_signal() {
let mut sb = MaterialSnackbar::new(Rect::new(0, 0, 375, 100));
sb.set_message("File deleted");
sb.set_action_text("Undo");
sb.show();
let fired = Arc::new(AtomicBool::new(false));
let f = fired.clone();
sb.action_pressed.connect(move || {
f.store(true, Ordering::SeqCst);
});
sb.handle_event(&Event::MousePress { pos: Point::new(340, 80), button: 1, modifiers: 0 });
sb.handle_event(&Event::MouseRelease { pos: Point::new(340, 80), button: 1 });
assert!(fired.load(Ordering::SeqCst));
}
#[test]
fn material_snackbar_hidden_blocks_events() {
let mut sb = MaterialSnackbar::new(Rect::new(0, 0, 375, 100));
sb.set_message("Test");
let fired = Arc::new(AtomicBool::new(false));
let f = fired.clone();
sb.dismissed.connect(move || {
f.store(true, Ordering::SeqCst);
});
sb.handle_event(&Event::MousePress { pos: Point::new(200, 80), button: 1, modifiers: 0 });
assert!(!fired.load(Ordering::SeqCst));
}
#[test]
fn material_snackbar_svg_output() {
let mut sb = MaterialSnackbar::new(Rect::new(0, 0, 375, 100));
sb.set_message("Hello");
sb.set_action_text("Action");
sb.show();
let svg = render_to_svg(&mut sb);
assert!(svg.starts_with("<svg"));
}
#[test]
fn material_snackbar_svg_output_hidden() {
let mut sb = MaterialSnackbar::new(Rect::new(0, 0, 375, 100));
let svg = render_to_svg(&mut sb);
assert!(svg.starts_with("<svg"));
}
#[test]
fn cupertino_alert_dialog_creation() {
let dlg = CupertinoAlertDialog::new(Rect::new(0, 0, 400, 300));
assert_eq!(dlg.kind(), WidgetKind::CupertinoAlertDialog);
assert!(dlg.title().is_empty());
assert!(dlg.message().is_empty());
}
#[test]
fn cupertino_alert_dialog_title_accessors() {
let mut dlg = CupertinoAlertDialog::new(Rect::new(0, 0, 400, 300));
dlg.set_title("Delete File");
assert_eq!(dlg.title(), "Delete File");
}
#[test]
fn cupertino_alert_dialog_message_accessors() {
let mut dlg = CupertinoAlertDialog::new(Rect::new(0, 0, 400, 300));
dlg.set_message("Are you sure?");
assert_eq!(dlg.message(), "Are you sure?");
}
#[test]
fn cupertino_alert_dialog_confirm_signal() {
let mut dlg = CupertinoAlertDialog::new(Rect::new(0, 0, 400, 300));
let fired = Arc::new(AtomicBool::new(false));
let f = fired.clone();
dlg.confirmed.connect(move || {
f.store(true, Ordering::SeqCst);
});
dlg.handle_event(&Event::MouseRelease { pos: Point::new(300, 225), button: 1 });
assert!(fired.load(Ordering::SeqCst));
}
#[test]
fn cupertino_alert_dialog_cancel_signal() {
let mut dlg = CupertinoAlertDialog::new(Rect::new(0, 0, 400, 300));
let fired = Arc::new(AtomicBool::new(false));
let f = fired.clone();
dlg.canceled.connect(move || {
f.store(true, Ordering::SeqCst);
});
dlg.handle_event(&Event::MouseRelease { pos: Point::new(100, 225), button: 1 });
assert!(fired.load(Ordering::SeqCst));
}
#[test]
fn cupertino_alert_dialog_svg_output() {
let mut dlg = CupertinoAlertDialog::new(Rect::new(0, 0, 400, 300));
dlg.set_title("Notice");
dlg.set_message("Hello world");
let svg = render_to_svg(&mut dlg);
assert!(svg.starts_with("<svg"));
}
#[test]
fn cupertino_slider_creation() {
let sl = CupertinoSlider::new(Rect::new(0, 0, 200, 40));
assert_eq!(sl.kind(), WidgetKind::CupertinoSlider);
assert_eq!(sl.value(), 0.0);
assert_eq!(sl.min(), 0.0);
assert_eq!(sl.max(), 1.0);
}
#[test]
fn cupertino_slider_value_get_set() {
let mut sl = CupertinoSlider::new(Rect::new(0, 0, 200, 40));
sl.set_value(0.5);
assert!((sl.value() - 0.5).abs() < 1e-6);
sl.set_value(2.0);
assert!((sl.value() - 1.0).abs() < 1e-6);
sl.set_value(-1.0);
assert!((sl.value() - 0.0).abs() < 1e-6);
}
#[test]
fn cupertino_slider_min_max_accessors() {
let mut sl = CupertinoSlider::new(Rect::new(0, 0, 200, 40));
sl.set_max(100.0);
sl.set_min(10.0);
assert!((sl.min() - 10.0).abs() < 1e-6);
assert!((sl.max() - 100.0).abs() < 1e-6);
sl.set_value(5.0);
assert!((sl.value() - 10.0).abs() < 1e-6);
}
#[test]
fn cupertino_slider_min_above_max_clamps_instead_of_panicking() {
let mut sl = CupertinoSlider::new(Rect::new(0, 0, 200, 40));
sl.set_min(3.5);
let value = sl.value();
assert!(value.is_finite(), "value must remain finite after crossed bounds, got {value}");
assert!(
(1.0..=3.5).contains(&value),
"value {value} is outside the ordered range [1.0, 3.5]"
);
let mut sl = CupertinoSlider::new(Rect::new(0, 0, 200, 40));
sl.set_min(5.0);
sl.set_max(2.0);
let value = sl.value();
assert!(value.is_finite(), "value must remain finite after crossed bounds, got {value}");
}
#[test]
fn cupertino_slider_nan_bound_does_not_poison_the_value() {
let mut sl = CupertinoSlider::new(Rect::new(0, 0, 200, 40));
sl.set_value(0.5);
sl.set_min(f32::NAN);
let value = sl.value();
assert!(!value.is_nan(), "a NaN bound must not make the value NaN; got {value}");
}
#[test]
fn cupertino_slider_mouse_press_updates_value() {
let mut sl = CupertinoSlider::new(Rect::new(0, 0, 200, 40));
sl.handle_event(&Event::MousePress { pos: Point::new(100, 20), button: 1, modifiers: 0 });
assert!((sl.value() - 0.5).abs() < 0.05);
}
#[test]
fn cupertino_slider_set_value_emits_changed_only_on_real_change() {
let mut sl = CupertinoSlider::new(Rect::new(0, 0, 200, 40));
let seen = Arc::new(AtomicUsize::new(0));
{
let c = seen.clone();
sl.value_changed.connect(move |_| {
c.fetch_add(1, Ordering::SeqCst);
});
}
sl.set_value(0.5);
assert_eq!(seen.load(Ordering::SeqCst), 1);
sl.set_value(0.5); assert_eq!(seen.load(Ordering::SeqCst), 1);
sl.set_value(0.9);
assert_eq!(seen.load(Ordering::SeqCst), 2);
}
#[test]
fn cupertino_slider_mouse_press_different_position() {
let mut sl = CupertinoSlider::new(Rect::new(0, 0, 200, 40));
sl.handle_event(&Event::MousePress { pos: Point::new(150, 20), button: 1, modifiers: 0 });
assert!((sl.value() - 0.78).abs() < 0.05);
}
#[test]
fn cupertino_slider_svg_output() {
let mut sl = CupertinoSlider::new(Rect::new(0, 0, 200, 40));
let svg = render_to_svg(&mut sl);
assert!(svg.starts_with("<svg"));
}
#[test]
fn material_navigation_rail_creation() {
let rail = MaterialNavigationRail::new(Rect::new(0, 0, 80, 400));
assert_eq!(rail.kind(), WidgetKind::MaterialNavigationRail);
assert_eq!(rail.item_count(), 0);
assert_eq!(rail.selected(), 0);
}
#[test]
fn material_navigation_rail_add_items() {
let mut rail = MaterialNavigationRail::new(Rect::new(0, 0, 80, 400));
rail.add_item("\u{1f3e0}", "Home");
rail.add_item("\u{2b50}", "Favorites");
rail.add_item("\u{2699}", "Settings");
assert_eq!(rail.item_count(), 3);
}
#[test]
fn material_navigation_rail_selection() {
let mut rail = MaterialNavigationRail::new(Rect::new(0, 0, 80, 400));
rail.add_item("A", "Item A");
rail.add_item("B", "Item B");
rail.add_item("C", "Item C");
assert_eq!(rail.selected(), 0);
rail.set_selected(1);
assert_eq!(rail.selected(), 1);
rail.set_selected(5); assert_eq!(rail.selected(), 2);
}
#[test]
fn material_navigation_rail_clear_items() {
let mut rail = MaterialNavigationRail::new(Rect::new(0, 0, 80, 400));
rail.add_item("A", "Item A");
rail.add_item("B", "Item B");
assert_eq!(rail.item_count(), 2);
rail.clear_items();
assert_eq!(rail.item_count(), 0);
assert_eq!(rail.selected(), 0);
}
#[test]
fn material_navigation_rail_svg_output() {
let mut rail = MaterialNavigationRail::new(Rect::new(0, 0, 80, 400));
rail.add_item("\u{1f3e0}", "Home");
rail.add_item("\u{2b50}", "Stars");
let svg = render_to_svg(&mut rail);
assert!(svg.starts_with("<svg"));
}
#[test]
fn material_navigation_rail_typed_item_source_is_checked_and_rendered() {
static RING: crate::widget::IconData = crate::widget::IconData {
name: "rail-ring",
grid: 24,
paths: &["M2-2H22V-22H2Z M7-7V-17H17V-7Z"],
};
let mut rail = MaterialNavigationRail::new(Rect::new(0, 0, 80, 400));
rail.add_item("legacy", "Home");
assert_eq!(
rail.set_item_icon_source(1, Some(IconSource::static_geometry(&RING))),
Err(IconItemError::IndexOutOfBounds { index: 1, len: 1 })
);
rail.set_item_icon_source(0, Some(IconSource::static_geometry(&RING)))
.expect("existing rail item accepts typed source");
assert!(render_to_svg(&mut rail).contains("fill-rule=\"nonzero\""));
rail.clear_items();
assert_eq!(
rail.set_item_icon_source(0, None),
Err(IconItemError::IndexOutOfBounds { index: 0, len: 0 })
);
}
#[test]
fn the_rail_strip_is_fixed_width_and_only_selects_inside_it() {
let mut rail = MaterialNavigationRail::new(Rect::new(0, 0, 240, 300));
rail.add_item("home", "Home");
rail.add_item("search", "Search");
assert_eq!(rail.selected(), 0);
rail.handle_event(&crate::event::Event::MouseRelease {
pos: Point::new(20, 80),
button: 1,
});
assert_eq!(rail.selected(), 1, "a press on the second destination selects it");
rail.handle_event(&crate::event::Event::MouseRelease {
pos: Point::new(200, 80),
button: 1,
});
assert_eq!(rail.selected(), 1, "a press past the strip's edge selects nothing");
}
}