use crate::compat::{format, String, ToString};
use crate::core::{Color, Font, HorizontalAlignment, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::{GenericSignal, Signal1};
use crate::widget::capability::coercion::{expect_bool, expect_f64, expect_i64, 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::numeric::ordered_clamp_f64;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
pub const SPIN_BOX_MAX_DECIMALS: u32 = 9;
pub struct SpinBox {
base: BaseWidget,
value: f64,
minimum: f64,
maximum: f64,
single_step: f64,
decimals: u32,
prefix: String,
suffix: String,
special_value_text: Option<String>,
wrapping: bool,
pub value_changed: Signal1<i32>,
pub editing_finished: GenericSignal,
}
impl SpinBox {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::SpinBox, geometry, "SpinBox"),
value: 0.0,
minimum: 0.0,
maximum: 99.0,
single_step: 1.0,
decimals: 0,
prefix: String::new(),
suffix: String::new(),
special_value_text: None,
wrapping: false,
value_changed: Signal1::new(),
editing_finished: GenericSignal::new(),
}
}
pub fn value(&self) -> i32 {
round_to_i32(self.value)
}
pub fn value_f64(&self) -> f64 {
self.value
}
pub fn set_value(&mut self, value: i32) {
self.set_value_f64(value as f64);
}
pub fn set_value_f64(&mut self, value: f64) {
if value.is_nan() {
return;
}
let clamped = ordered_clamp_f64(value, self.minimum, self.maximum);
let rounded = round_to_decimals(clamped, self.decimals);
if self.value == rounded {
return;
}
self.value = rounded;
self.value_changed.emit(self.value());
self.base.request_redraw();
}
pub fn minimum(&self) -> i32 {
round_to_i32(self.minimum)
}
pub fn minimum_f64(&self) -> f64 {
self.minimum
}
pub fn set_minimum(&mut self, minimum: i32) {
self.set_minimum_f64(minimum as f64);
}
pub fn set_minimum_f64(&mut self, minimum: f64) {
if minimum.is_nan() {
return;
}
self.minimum = minimum;
if self.maximum < self.minimum {
self.maximum = self.minimum;
}
self.set_value_f64(self.value); self.base.request_redraw();
}
pub fn maximum(&self) -> i32 {
round_to_i32(self.maximum)
}
pub fn maximum_f64(&self) -> f64 {
self.maximum
}
pub fn set_maximum(&mut self, maximum: i32) {
self.set_maximum_f64(maximum as f64);
}
pub fn set_maximum_f64(&mut self, maximum: f64) {
if maximum.is_nan() {
return;
}
self.maximum = maximum;
if self.minimum > self.maximum {
self.minimum = self.maximum;
}
self.set_value_f64(self.value); self.base.request_redraw();
}
pub fn set_range(&mut self, minimum: i32, maximum: i32) {
self.set_range_f64(minimum as f64, maximum as f64);
}
pub fn set_range_f64(&mut self, minimum: f64, maximum: f64) {
if minimum.is_nan() || maximum.is_nan() {
return;
}
self.minimum = minimum.min(maximum);
self.maximum = minimum.max(maximum);
self.set_value_f64(self.value); self.base.request_redraw();
}
pub fn decimals(&self) -> u32 {
self.decimals
}
pub fn set_decimals(&mut self, decimals: u32) {
let decimals = decimals.min(SPIN_BOX_MAX_DECIMALS);
if self.decimals == decimals {
return;
}
self.decimals = decimals;
self.value = round_to_decimals(self.value, decimals);
self.base.request_redraw();
}
pub fn single_step(&self) -> i32 {
round_to_i32(self.single_step)
}
pub fn single_step_f64(&self) -> f64 {
self.single_step
}
pub fn set_single_step(&mut self, step: i32) {
self.set_single_step_f64(step as f64);
}
pub fn set_single_step_f64(&mut self, step: f64) {
let floor = self.smallest_step();
self.single_step = if step.is_nan() { floor } else { step.abs().max(floor) };
self.base.request_redraw();
}
fn smallest_step(&self) -> f64 {
if self.decimals == 0 {
1.0
} else {
10f64.powi(-(self.decimals as i32))
}
}
pub fn prefix(&self) -> &str {
&self.prefix
}
pub fn set_prefix(&mut self, prefix: String) {
self.prefix = prefix;
self.base.request_redraw();
}
pub fn suffix(&self) -> &str {
&self.suffix
}
pub fn set_suffix(&mut self, suffix: String) {
self.suffix = suffix;
self.base.request_redraw();
}
pub fn special_value_text(&self) -> Option<&str> {
self.special_value_text.as_deref()
}
pub fn set_special_value_text(&mut self, text: Option<String>) {
self.special_value_text = text;
self.base.request_redraw();
}
pub fn wrapping(&self) -> bool {
self.wrapping
}
pub fn set_wrapping(&mut self, wrapping: bool) {
self.wrapping = wrapping;
self.base.request_redraw();
}
pub fn step_up(&mut self) {
let mut new_value = self.value + self.single_step;
if new_value > self.maximum {
if self.wrapping {
new_value = self.minimum;
} else {
new_value = self.maximum;
}
}
self.set_value_f64(new_value);
}
pub fn step_down(&mut self) {
let mut new_value = self.value - self.single_step;
if new_value < self.minimum {
if self.wrapping {
new_value = self.maximum;
} else {
new_value = self.minimum;
}
}
self.set_value_f64(new_value);
}
pub fn formatted_value(&self) -> String {
match self.decimals {
0 => format!("{}", round_to_i32(self.value)),
n => format!("{:.*}", n as usize, self.value),
}
}
fn display_text(&self) -> String {
if let Some(special) = &self.special_value_text {
if self.value == self.minimum {
return special.clone();
}
}
format!("{}{}{}", self.prefix, self.formatted_value(), self.suffix)
}
}
fn round_to_i32(value: f64) -> i32 {
if value.is_nan() {
return 0;
}
let rounded = value.round();
if rounded >= i32::MAX as f64 {
i32::MAX
} else if rounded <= i32::MIN as f64 {
i32::MIN
} else {
rounded as i32
}
}
fn round_to_decimals(value: f64, decimals: u32) -> f64 {
if decimals == 0 {
let rounded = value.round();
return if rounded == 0.0 { 0.0 } else { rounded };
}
if !value.is_finite() {
return value;
}
let text = format!("{:.*}", decimals as usize, value);
text.parse::<f64>().unwrap_or(value)
}
impl Widget for SpinBox {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
let val_w = self.formatted_value().len() as u32 * 10 + 25;
Size::new(val_w.max(60), 24)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for SpinBox {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
let as_number = |value: f64| {
if self.decimals == 0 {
CapabilityValue::Int(round_to_i32(value) as i64)
} else {
CapabilityValue::Float(value)
}
};
match name {
"minimum" => Ok(as_number(self.minimum_f64())),
"maximum" => Ok(as_number(self.maximum_f64())),
"value" => Ok(as_number(self.value_f64())),
"single_step" => Ok(as_number(self.single_step_f64())),
"decimals" => Ok(CapabilityValue::Int(self.decimals() as i64)),
"prefix" => Ok(CapabilityValue::String(self.prefix().to_string())),
"suffix" => Ok(CapabilityValue::String(self.suffix().to_string())),
"special_value_text" => match self.special_value_text() {
Some(text) => Ok(CapabilityValue::String(text.to_string())),
None => Ok(CapabilityValue::Null),
},
"wrapping" => Ok(CapabilityValue::Bool(self.wrapping())),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
fn number(value: CapabilityValue) -> Result<f64, CapabilityAccessError> {
match value {
CapabilityValue::Int(int) => Ok(int as f64),
other => expect_f64(other),
}
}
match name {
"minimum" => {
self.set_minimum_f64(number(value)?);
Ok(())
}
"maximum" => {
self.set_maximum_f64(number(value)?);
Ok(())
}
"value" => {
self.set_value_f64(number(value)?);
Ok(())
}
"single_step" => {
self.set_single_step_f64(number(value)?);
Ok(())
}
"decimals" => {
let decimals = expect_i64(value)?;
let decimals =
u32::try_from(decimals).map_err(|_| CapabilityAccessError::OutOfRange)?;
self.set_decimals(decimals);
Ok(())
}
"prefix" => {
self.set_prefix(expect_string(value)?);
Ok(())
}
"suffix" => {
self.set_suffix(expect_string(value)?);
Ok(())
}
"special_value_text" => {
match value {
CapabilityValue::Null => self.set_special_value_text(None),
other => self.set_special_value_text(Some(expect_string(other)?)),
}
Ok(())
}
"wrapping" => {
self.set_wrapping(expect_bool(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"minimum",
"maximum",
"value",
"single_step",
"decimals",
"prefix",
"suffix",
"special_value_text",
"wrapping",
BASE_PROPERTY_NAMES
]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"step_up" => {
self.step_up();
Ok(())
}
"step_down" => {
self.step_down();
Ok(())
}
"set_range" | "set_value" => Err(CapabilityAccessError::OutOfRange),
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
impl SpinBox {
fn handle_button_click(&mut self, pos: &Point, rect: Rect, button_width: u32) {
if pos.x as f32 >= rect.x as f32 + rect.width as f32 - button_width as f32 * 2.0 {
if (pos.x as f32) < rect.x as f32 + rect.width as f32 - button_width as f32 {
self.step_down();
} else {
self.step_up();
}
self.base.clicked.emit();
}
}
}
impl EventHandler for SpinBox {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match event {
Event::MousePress { pos, button } => {
let rect = self.geometry();
let button_width = 20;
if *button == 1 {
self.handle_button_click(pos, rect, button_width);
}
}
#[cfg(feature = "touch")]
Event::TouchBegin { pos, .. } => {
let rect = self.geometry();
let button_width = 20;
self.handle_button_click(pos, rect, button_width);
}
Event::KeyPress { key, modifiers: _ } => {
match *key {
38 => {
self.step_up();
}
40 => {
self.step_down();
}
13 => {
self.editing_finished.emit();
}
27 => {
self.editing_finished.emit();
}
_ => {}
}
}
Event::FocusLost => {
self.editing_finished.emit();
}
_ => {}
}
}
}
impl Draw for SpinBox {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let padding = 4;
let button_width = 20;
let text_x = rect.x + padding;
let text_y = rect.y as f32 + rect.height as f32 / 2.0;
let style = self.style();
let bg = style.background_color.unwrap_or(Color::rgb(255, 255, 255));
let text_color = style.text_color.unwrap_or(Color::rgb(0, 0, 0));
let button_bg = style.background_color.unwrap_or(Color::rgb(240, 240, 240));
let button_border = style.border_color.unwrap_or(Color::rgb(200, 200, 200));
let arrow_color = style.text_color.unwrap_or(Color::rgb(100, 100, 100));
let default_font = Font::default();
let font = style.font.as_ref().unwrap_or(&default_font);
context.fill_rect(Rect::new(rect.x, rect.y, rect.width, rect.height), bg);
if let Some(border_color) = style.border_color {
context.draw_rect(Rect::new(rect.x, rect.y, rect.width, rect.height), border_color);
}
let down_button_x_f = rect.x as f32 + rect.width as f32 - button_width as f32 * 2.0;
let up_button_x_f = rect.x as f32 + rect.width as f32 - button_width as f32;
let button_width_f = button_width as f32;
let rect_y_f = rect.y as f32;
let rect_height_f = rect.height as f32;
context.fill_rect(
Rect::from_f32(down_button_x_f, rect_y_f, button_width_f, rect_height_f),
button_bg,
);
context.draw_rect(
Rect::from_f32(down_button_x_f, rect_y_f, button_width_f, rect_height_f),
button_border,
);
let down_arrow_x_f = down_button_x_f + button_width_f / 2.0;
let down_arrow_y_f = rect_y_f + rect_height_f / 2.0;
let arrow_size = 4;
let arrow_size_f = arrow_size as f32;
context.draw_line(
Point::from_f32(down_arrow_x_f - arrow_size_f, down_arrow_y_f - arrow_size_f / 2.0),
Point::from_f32(down_arrow_x_f + arrow_size_f, down_arrow_y_f - arrow_size_f / 2.0),
arrow_color,
);
context.draw_line(
Point::from_f32(down_arrow_x_f + arrow_size_f, down_arrow_y_f + arrow_size_f / 2.0),
Point::from_f32(down_arrow_x_f, down_arrow_y_f + arrow_size_f / 2.0),
arrow_color,
);
context.draw_line(
Point::from_f32(down_arrow_x_f, down_arrow_y_f + arrow_size_f / 2.0),
Point::from_f32(down_arrow_x_f - arrow_size_f, down_arrow_y_f - arrow_size_f / 2.0),
arrow_color,
);
context.fill_rect(
Rect::from_f32(up_button_x_f, rect_y_f, button_width_f, rect_height_f),
button_bg,
);
context.draw_rect(
Rect::from_f32(up_button_x_f, rect_y_f, button_width_f, rect_height_f),
button_border,
);
let up_arrow_x_f = up_button_x_f + button_width_f / 2.0;
let up_arrow_y_f = rect_y_f + rect_height_f / 2.0;
context.draw_line(
Point::from_f32(up_arrow_x_f - arrow_size_f, up_arrow_y_f + arrow_size_f / 2.0),
Point::from_f32(up_arrow_x_f + arrow_size_f, up_arrow_y_f + arrow_size_f / 2.0),
arrow_color,
);
context.draw_line(
Point::from_f32(up_arrow_x_f + arrow_size_f, up_arrow_y_f + arrow_size_f / 2.0),
Point::from_f32(up_arrow_x_f, up_arrow_y_f - arrow_size_f / 2.0),
arrow_color,
);
context.draw_line(
Point::from_f32(up_arrow_x_f, up_arrow_y_f - arrow_size_f / 2.0),
Point::from_f32(up_arrow_x_f - arrow_size_f, up_arrow_y_f + arrow_size_f / 2.0),
arrow_color,
);
let display_text = self.display_text();
if !display_text.is_empty() {
context.draw_text(
Point::new(text_x, text_y as i32),
&display_text,
font,
text_color,
HorizontalAlignment::Left,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Rect;
#[test]
fn spinbox_creation_defaults() {
let sb = SpinBox::new(Rect::new(0, 0, 100, 24));
assert_eq!(sb.value(), 0);
assert_eq!(sb.minimum(), 0);
assert_eq!(sb.maximum(), 99);
assert_eq!(sb.single_step(), 1);
assert!(sb.prefix().is_empty());
assert!(sb.suffix().is_empty());
assert!(!sb.wrapping());
assert!(sb.special_value_text().is_none());
}
#[test]
fn spinbox_set_value() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_value(50);
assert_eq!(sb.value(), 50);
sb.set_value(200); assert_eq!(sb.value(), 99);
sb.set_value(-10); assert_eq!(sb.value(), 0);
}
#[test]
fn spinbox_set_range() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_minimum(-50);
sb.set_maximum(200);
assert_eq!(sb.minimum(), -50);
assert_eq!(sb.maximum(), 200);
}
#[test]
fn spinbox_set_range_reclamps_value() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_value(50);
sb.set_range(60, 100);
assert_eq!(sb.value(), 60);
}
#[test]
fn spinbox_prefix_suffix() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_prefix("$".to_string());
assert_eq!(sb.prefix(), "$");
sb.set_suffix(" USD".to_string());
assert_eq!(sb.suffix(), " USD");
sb.set_prefix(String::new());
assert!(sb.prefix().is_empty());
}
#[test]
fn spinbox_single_step() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_single_step(5);
assert_eq!(sb.single_step(), 5);
sb.set_single_step(0); assert_eq!(sb.single_step(), 1);
}
#[test]
fn spinbox_wrapping() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
assert!(!sb.wrapping());
sb.set_wrapping(true);
assert!(sb.wrapping());
sb.set_wrapping(false);
assert!(!sb.wrapping());
}
#[test]
fn spinbox_step_up() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_value(50);
sb.step_up();
assert_eq!(sb.value(), 51);
}
#[test]
fn spinbox_step_up_clamps() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_value(99);
sb.step_up();
assert_eq!(sb.value(), 99); }
#[test]
fn spinbox_step_up_wraps() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_wrapping(true);
sb.set_value(99);
sb.step_up();
assert_eq!(sb.value(), 0); }
#[test]
fn spinbox_step_down() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_value(50);
sb.step_down();
assert_eq!(sb.value(), 49);
}
#[test]
fn spinbox_step_down_clamps() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_value(0);
sb.step_down();
assert_eq!(sb.value(), 0); }
#[test]
fn spinbox_step_down_wraps() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_wrapping(true);
sb.set_value(0);
sb.step_down();
assert_eq!(sb.value(), 99); }
#[test]
fn spinbox_special_value_text() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
assert!(sb.special_value_text().is_none());
sb.set_special_value_text(Some("Zero".to_string()));
assert_eq!(sb.special_value_text(), Some("Zero"));
sb.set_special_value_text(None);
assert!(sb.special_value_text().is_none());
}
#[test]
fn spinbox_geometry_delegation() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_geometry(Rect::new(10, 10, 200, 30));
assert_eq!(sb.geometry(), Rect::new(10, 10, 200, 30));
}
#[test]
fn spinbox_visibility() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
assert!(sb.is_visible());
sb.hide();
assert!(!sb.is_visible());
sb.show();
assert!(sb.is_visible());
}
#[test]
fn spinbox_enabled() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
assert!(sb.is_enabled());
sb.set_enabled(false);
assert!(!sb.is_enabled());
sb.set_enabled(true);
assert!(sb.is_enabled());
}
#[test]
fn spinbox_id_kind() {
let sb_a = SpinBox::new(Rect::new(0, 0, 100, 24));
let sb_b = SpinBox::new(Rect::new(0, 0, 100, 24));
assert_ne!(sb_a.id(), sb_b.id());
assert_eq!(sb_a.kind(), WidgetKind::SpinBox);
assert_eq!(sb_b.kind(), WidgetKind::SpinBox);
}
#[test]
fn spinbox_signal_accessors() {
let sb = SpinBox::new(Rect::new(0, 0, 100, 24));
let _value_changed = &sb.value_changed;
let _editing_finished = &sb.editing_finished;
}
#[test]
fn spinbox_defaults_to_integer_precision() {
let sb = SpinBox::new(Rect::new(0, 0, 100, 24));
assert_eq!(sb.decimals(), 0);
assert_eq!(sb.formatted_value(), "0", "integer mode must print no decimals");
}
#[test]
fn spinbox_decimal_round_trip() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_decimals(2);
sb.set_value_f64(1.5);
assert_eq!(sb.value_f64(), 1.5);
assert_eq!(sb.value(), 2, "the integer reading rounds, it does not truncate");
assert_eq!(sb.formatted_value(), "1.50", "a two-decimal box shows two decimals");
}
#[test]
fn spinbox_decimal_step() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_decimals(2);
sb.set_single_step_f64(0.25);
sb.set_value_f64(1.0);
sb.step_up();
assert_eq!(sb.value_f64(), 1.25);
sb.step_down();
sb.step_down();
assert_eq!(sb.value_f64(), 0.75);
}
#[test]
fn spinbox_decimal_step_floor_is_the_last_place() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_decimals(2);
sb.set_single_step_f64(0.0);
assert_eq!(sb.single_step_f64(), 0.01);
sb.set_single_step_f64(-0.5);
assert_eq!(sb.single_step_f64(), 0.5, "a negative step is a magnitude, not a direction");
}
#[test]
fn spinbox_set_decimals_rerounds_the_value() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_decimals(4);
sb.set_value_f64(1.23456);
assert_eq!(sb.value_f64(), 1.2346);
sb.set_decimals(2);
assert_eq!(sb.value_f64(), 1.23);
sb.set_decimals(0);
assert_eq!(sb.value_f64(), 1.0);
}
#[test]
fn spinbox_stored_value_equals_the_printed_value() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_decimals(3);
sb.set_value_f64(2.0965);
assert_eq!(
sb.formatted_value(),
"2.096",
"the printed digits must be the correctly-rounded ones"
);
assert_eq!(
sb.value_f64(),
2.096,
"and the stored value must be exactly the number that was printed — the scale \
form would store 2.097 here and disagree with its own display"
);
}
#[test]
fn spinbox_rounds_a_below_midpoint_literal_down() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_decimals(2);
sb.set_value_f64(1.005);
assert_eq!(
sb.formatted_value(),
"1.00",
"1.005 as an f64 is 1.004999...; 1.00 is the correct rounding of the value that was \
actually stored, not a rounding failure"
);
assert_eq!(sb.value_f64(), 1.0);
let parsed: f64 = "1.005".parse().unwrap();
assert_eq!(parsed, 1.005, "the literal and the parsed text must be the same f64");
}
#[test]
fn spinbox_rejects_nan_and_keeps_the_previous_value() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_value_f64(3.0);
sb.set_value_f64(f64::NAN);
assert_eq!(sb.value_f64(), 3.0);
sb.set_minimum_f64(f64::NAN);
assert_eq!(sb.minimum_f64(), 0.0);
sb.set_maximum_f64(f64::NAN);
assert_eq!(sb.maximum_f64(), 99.0);
}
#[test]
fn spinbox_infinite_bounds_are_a_valid_range() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_maximum_f64(f64::INFINITY);
sb.set_range_f64(0.0, f64::INFINITY);
sb.set_value_f64(1.0e9);
assert_eq!(sb.value_f64(), 1.0e9);
}
#[test]
fn spinbox_crossed_decimal_bounds_are_ordered() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_range_f64(10.0, 0.0);
assert_eq!(sb.minimum_f64(), 0.0);
assert_eq!(sb.maximum_f64(), 10.0);
sb.set_value_f64(20.0);
assert_eq!(sb.value_f64(), 10.0);
}
#[test]
fn spinbox_negative_zero_rounds_to_zero() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_minimum(-5);
sb.set_value_f64(-0.4);
assert_eq!(sb.value_f64(), 0.0);
assert_eq!(sb.formatted_value(), "0", "a zero must not be printed as `-0`");
}
#[test]
fn spinbox_value_property_switches_carrier_with_decimals() {
use crate::widget::capability::WidgetProperties;
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
assert_eq!(sb.get("value").unwrap(), CapabilityValue::Int(0));
assert_eq!(sb.set("value", CapabilityValue::Int(7)), Ok(()));
assert_eq!(sb.get("value").unwrap(), CapabilityValue::Int(7));
sb.set("decimals", CapabilityValue::Int(2)).unwrap();
assert_eq!(sb.get("decimals").unwrap(), CapabilityValue::Int(2));
assert_eq!(sb.set("value", CapabilityValue::Float(2.5)), Ok(()));
assert_eq!(sb.get("value").unwrap(), CapabilityValue::Float(2.5));
assert_eq!(sb.set("value", CapabilityValue::Int(3)), Ok(()));
assert_eq!(sb.get("value").unwrap(), CapabilityValue::Float(3.0));
}
#[test]
fn spinbox_decimals_are_bounded_and_negative_is_refused() {
use crate::widget::capability::WidgetProperties;
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_decimals(99);
assert_eq!(sb.decimals(), SPIN_BOX_MAX_DECIMALS);
assert_eq!(
sb.set("decimals", CapabilityValue::Int(-1)),
Err(CapabilityAccessError::OutOfRange),
"a negative precision is a value error, not a silently accepted magnitude"
);
assert_eq!(sb.decimals(), SPIN_BOX_MAX_DECIMALS, "the refused write must not take effect");
}
#[test]
fn spinbox_size_hint_accounts_for_decimals() {
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_value(1234);
let integer_width = sb.size_hint().width;
sb.set_decimals(2);
assert!(
sb.size_hint().width > integer_width,
"a two-decimal box showing 1234.00 must be wider than one showing 1234"
);
}
#[test]
fn spinbox_set_decimals_does_not_emit_value_changed() {
use crate::compat::Arc;
use core::sync::atomic::{AtomicU32, Ordering};
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_decimals(3);
sb.set_value_f64(1.2345);
let seen = Arc::new(AtomicU32::new(0));
let counter = seen.clone();
sb.value_changed.connect(move |_| {
counter.fetch_add(1, Ordering::SeqCst);
});
sb.set_decimals(1);
assert_eq!(
seen.load(Ordering::SeqCst),
0,
"re-rounding on a precision change is not a value change"
);
assert_eq!(sb.value_f64(), 1.2);
sb.step_up();
assert_eq!(seen.load(Ordering::SeqCst), 1, "a real step still emits");
}
}