use crate::compat::{format, String, ToString};
use crate::core::{Color, Font, HorizontalAlignment, Point, Rect, Size};
use crate::event::{Event, EventHandler};
#[cfg(full_widgets)]
use crate::layout::{
AlignItems, FlexDirection, FlexLayout, FlexWrap, JustifyContent, LayoutParams,
};
use crate::render::RenderContext;
use crate::signal::{GenericSignal, Signal1};
use crate::style::EdgeOffsets;
#[cfg(full_widgets)]
use crate::widget::composite::CompositeBuilder;
use crate::widget::decorations::{
DecorationLayout, DecorationMetrics, DecorationSlots, DECORATION_GAP,
};
use crate::widget::metrics::{dimensions, ControlMetrics};
use crate::widget::capability::coercion::{
expect_bool, expect_f64, expect_i64, expect_string, expect_text_direction,
text_direction_to_str,
};
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;
#[cfg(full_widgets)]
use crate::widget::WidgetFactory;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
pub const SPIN_BOX_MAX_DECIMALS: u32 = 9;
const SPIN_BOX_BUTTON_WIDTH: u32 = dimensions::SPIN_BOX_STEP_BUTTON_WIDTH;
const SPIN_BOX_BUTTONS: u32 = dimensions::SPIN_BOX_STEP_BUTTONS;
const SPIN_BOX_ARROW_HALF_SPAN: f32 = 4.0;
const SPIN_BOX_ARROW_DROP: f32 = 5.0;
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,
direction: crate::core::TextDirection,
pub value_changed: Signal1<i32>,
pub editing_finished: GenericSignal,
}
impl SpinBox {
fn row_band(&self) -> Rect {
ControlMetrics::full_width_band(self.geometry(), dimensions::TEXT_FIELD_MIN_HEIGHT)
}
fn button_column(&self) -> Rect {
self.assemble_row().1
}
fn editable_rect(&self) -> Rect {
self.assemble_row().0
}
fn up_button(&self) -> Rect {
let column = self.button_column();
let half = column.height / 2;
Rect::new(column.x, column.y, column.width, half)
}
fn down_button(&self) -> Rect {
let column = self.button_column();
let half = column.height / 2;
Rect::new(
column.x,
column.y + column.height.saturating_sub(half) as i32,
column.width,
half,
)
}
fn assemble_row(&self) -> (Rect, Rect) {
let band = self.row_band();
let column_width = self.step_column_width();
let rtl = self.direction.is_right_to_left();
let column_x =
if rtl { band.x } else { band.x + band.width.saturating_sub(column_width) as i32 };
let value_x = if rtl { band.x + column_width as i32 } else { band.x };
#[cfg(not(full_widgets))]
{
(
Rect::new(value_x, band.y, band.width.saturating_sub(column_width), band.height),
Rect::new(column_x, band.y, column_width, band.height),
)
}
#[cfg(full_widgets)]
{
let factory = WidgetFactory::new_with_defaults();
let mut row = CompositeBuilder::new(
Box::new(FlexLayout::with_params(
if rtl { FlexDirection::RowReverse } else { FlexDirection::Row },
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::Stretch,
0,
0,
)),
EdgeOffsets::all(0),
Size::new(0, 0),
);
let value = row.add_sized(
&factory,
"label",
"",
Size::new(dimensions::TEXT_FIELD_PADDING_H, band.height),
LayoutParams::filled(),
);
debug_assert!(value.is_some(), "the value column is a core control");
let column = row.add_sized(
&factory,
"label",
"",
Size::new(column_width, band.height),
LayoutParams::new(),
);
debug_assert!(column.is_some(), "the step column is a core control");
let mut placed: Vec<Rect> = Vec::with_capacity(2);
row.arrange(band, &mut |_, rect| placed.push(rect));
match (placed.first(), placed.get(1)) {
(Some(value), Some(column)) => (*value, *column),
_ => {
let column_width = column_width.min(band.width);
(
Rect::new(
value_x,
band.y,
band.width.saturating_sub(column_width),
band.height,
),
Rect::new(column_x, band.y, column_width, band.height),
)
}
}
}
}
fn step_column_width(&self) -> u32 {
dimensions::SPIN_BOX_STEP_BUTTON_WIDTH
.saturating_mul(SPIN_BOX_BUTTONS)
.min(self.row_band().width)
}
pub fn direction(&self) -> crate::core::TextDirection {
self.direction
}
pub fn set_direction(&mut self, direction: crate::core::TextDirection) {
if self.direction == direction {
return;
}
self.direction = direction;
self.base.request_redraw();
self.base.request_layout();
}
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,
direction: crate::core::TextDirection::LeftToRight,
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),
}
}
pub 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 value_text(&self) -> String {
if let Some(special) = &self.special_value_text {
if self.value == self.minimum {
return special.clone();
}
}
self.formatted_value()
}
}
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 value = self.formatted_value();
let text_width = value.len() as u32 * 8;
let field_air = (dimensions::TEXT_FIELD_MIN_HEIGHT / 2).saturating_sub(8);
let padding = EdgeOffsets {
top: field_air,
right: dimensions::TEXT_FIELD_PADDING_H,
bottom: field_air,
left: dimensions::TEXT_FIELD_PADDING_H,
};
let floor = Size::new(
dimensions::TEXT_FIELD_PADDING_H * 2 + SPIN_BOX_BUTTON_WIDTH * SPIN_BOX_BUTTONS,
dimensions::TEXT_FIELD_MIN_HEIGHT,
);
ControlMetrics::implicit_size(Size::new(text_width, 0), padding, floor)
}
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())),
"display_text" => Ok(CapabilityValue::String(self.display_text())),
"value_text" => Ok(CapabilityValue::String(self.value_text())),
"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())),
"direction" => {
Ok(CapabilityValue::String(text_direction_to_str(self.direction()).to_string()))
}
_ => 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(())
}
"direction" => {
self.set_direction(expect_text_direction(value)?);
Ok(())
}
"display_text" | "value_text" => Err(CapabilityAccessError::ReadOnlyProperty),
_ => 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",
"direction",
"display_text",
"value_text",
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) {
if self.down_button().contains_point(pos) {
self.step_down();
self.base.clicked.emit();
} else if self.up_button().contains_point(pos) {
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 } => {
if *button == 1 {
self.handle_button_click(*pos);
}
}
#[cfg(feature = "touch")]
Event::TouchBegin { pos, .. } => {
self.handle_button_click(*pos);
}
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 band = self.row_band();
if band.width == 0 || band.height == 0 {
return;
}
let editable = self.editable_rect();
let down_button = self.down_button();
let up_button = self.up_button();
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(band, bg);
if let Some(border_color) = style.border_color {
context.draw_rect(band, border_color);
}
context.fill_rect(down_button, button_bg);
context.draw_rect(down_button, button_border);
draw_step_arrow(context, down_button, false, arrow_color);
context.fill_rect(up_button, button_bg);
context.draw_rect(up_button, button_border);
draw_step_arrow(context, up_button, true, arrow_color);
let slots = DecorationSlots {
prefix: self.prefix.clone(),
suffix: self.suffix.clone(),
..Default::default()
};
let metrics =
DecorationMetrics::measure(&slots, |text| context.measure_text(text, font).width);
let line_height = font.effective_line_height().max(1.0) as u32;
let layout = DecorationLayout::compute(
editable,
dimensions::TEXT_FIELD_PADDING_H,
line_height,
DECORATION_GAP,
metrics,
0,
&slots,
);
let value_text = self.value_text();
if !value_text.is_empty() {
let line = context.text_line(editable, font);
let align = if self.direction.is_right_to_left() {
HorizontalAlignment::Right
} else {
HorizontalAlignment::Left
};
context.draw_text_fitted(
Rect::new(layout.value.x, line.y, layout.value.width, line.height),
&value_text,
font,
text_color,
align,
);
}
let slot_color = text_color.blend(&bg, 0.35);
let line = context.text_line(editable, font);
if let Some(prefix_box) = layout.prefix {
context.draw_text(
Point::new(prefix_box.x, line.y),
&self.prefix,
font,
slot_color,
HorizontalAlignment::Left,
);
}
if let Some(suffix_box) = layout.suffix {
context.draw_text(
Point::new(suffix_box.x, line.y),
&self.suffix,
font,
slot_color,
HorizontalAlignment::Left,
);
}
}
}
fn draw_step_arrow(context: &mut RenderContext, button: Rect, up: bool, color: Color) {
let cx = button.x as f32 + button.width as f32 / 2.0;
let cy = button.y as f32 + button.height as f32 / 2.0;
let span = SPIN_BOX_ARROW_HALF_SPAN;
let drop = if up { -SPIN_BOX_ARROW_DROP } else { SPIN_BOX_ARROW_DROP };
let left = Point::from_f32(cx - span, cy - drop / 2.0);
let tip = Point::from_f32(cx, cy + drop / 2.0);
let right = Point::from_f32(cx + span, cy - drop / 2.0);
context.draw_line(left, tip, color);
context.draw_line(tip, right, color);
context.draw_line(right, left, color);
}
#[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 the_announcement_string_carries_the_units_and_the_drawn_value_does_not() {
use crate::widget::capability::WidgetProperties;
let mut sb = SpinBox::new(Rect::new(0, 0, 100, 24));
sb.set_value(12);
assert_eq!(sb.value_text(), "12", "the drawn value is the number alone");
assert_eq!(sb.display_text(), "12", "and with no units the two agree");
sb.set_prefix("$".to_string());
sb.set_suffix(" USD".to_string());
assert_eq!(sb.value_text(), "12", "the units are not part of the value");
assert_eq!(sb.display_text(), "$12 USD", "but they are part of the announcement");
assert_eq!(sb.get("display_text").unwrap().as_str(), Some("$12 USD"));
assert_eq!(sb.get("value_text").unwrap().as_str(), Some("12"));
assert!(sb.set("display_text", CapabilityValue::String("x".into())).is_err());
assert!(sb.set("value_text", CapabilityValue::String("x".into())).is_err());
}
#[test]
fn the_slots_are_drawn_on_either_side_of_the_value() {
let _theme_guard = crate::style::theme_test_guard();
let mut sb = SpinBox::new(Rect::new(0, 0, 200, 24));
sb.set_value(7);
sb.set_prefix("$".to_string());
sb.set_suffix("%".to_string());
let svg = crate::widget::svg::render_to_svg(&mut sb);
let mut runs = crate::widget::svg::text_ink_boxes(&svg);
runs.sort_by_key(|b| b.0);
assert_eq!(runs.len(), 3, "a prefix, a value and a suffix are three runs: {runs:?}");
assert!(
runs[0].2 <= runs[1].0,
"the prefix ({:?}) must end before the value ({:?}) begins",
runs[0],
runs[1]
);
assert!(
runs[1].2 <= runs[2].0,
"the value ({:?}) must end before the suffix ({:?}) begins",
runs[1],
runs[2]
);
let mut bare = SpinBox::new(Rect::new(0, 0, 200, 24));
bare.set_value(7);
let bare_svg = crate::widget::svg::render_to_svg(&mut bare);
assert_eq!(crate::widget::svg::text_ink_boxes(&bare_svg).len(), 1);
}
#[test]
fn a_prefix_wide_enough_to_fill_the_field_leaves_the_value_no_room() {
let _theme_guard = crate::style::theme_test_guard();
let mut sb = SpinBox::new(Rect::new(0, 0, 200, 24));
sb.set_value(7);
assert_eq!(sb.value_text(), "7");
let mut bare = SpinBox::new(Rect::new(0, 0, 200, 24));
bare.set_value(7);
let bare_runs =
crate::widget::svg::text_ink_boxes(&crate::widget::svg::render_to_svg(&mut bare));
assert_eq!(bare_runs.len(), 1, "the value is drawn when nothing squeezes it");
sb.set_prefix("a-very-long-prefix".to_string());
let runs = crate::widget::svg::text_ink_boxes(&crate::widget::svg::render_to_svg(&mut sb));
assert_eq!(
runs.len(),
1,
"the value's box clamped to zero width, so only the prefix has ink: {runs:?}"
);
assert!(
runs[0].0 >= sb.editable_rect().x + dimensions::TEXT_FIELD_PADDING_H as i32,
"the prefix must not escape the field: {runs:?}"
);
}
#[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_range(0, 1_000_000);
sb.set_value(1234);
assert_eq!(sb.formatted_value(), "1234", "the value must survive the setup");
let integer_width = sb.size_hint().width;
sb.set_decimals(2);
assert_eq!(sb.formatted_value(), "1234.00", "two more digits than the integer form");
assert!(
sb.size_hint().width > integer_width,
"a two-decimal box showing 1234.00 must be wider than one showing 1234"
);
}
#[test]
fn the_reported_height_is_the_band_that_is_painted() {
let sb = SpinBox::new(Rect::new(0, 0, 240, 120));
let band = sb.row_band();
assert_eq!(
band.height,
dimensions::TEXT_FIELD_MIN_HEIGHT,
"the painted band is the field's own height, whatever rectangle the control was given"
);
assert_eq!(
sb.size_hint().height,
band.height,
"a layout must be told the height the control actually draws"
);
assert_eq!(band.width, 240);
assert_eq!(band.y, (120 - dimensions::TEXT_FIELD_MIN_HEIGHT as i32) / 2);
}
#[test]
fn the_value_box_ends_where_the_button_column_begins() {
let narrowest =
(SPIN_BOX_BUTTON_WIDTH * SPIN_BOX_BUTTONS) + dimensions::TEXT_FIELD_PADDING_H;
for width in [narrowest, 64, 240, 400] {
let sb = SpinBox::new(Rect::new(0, 0, width, 120));
let editable = sb.editable_rect();
let column = sb.button_column();
assert_eq!(
editable.x + editable.width as i32,
column.x,
"the two boxes must share an edge at control width {width}"
);
assert_eq!(
editable.width + column.width,
sb.row_band().width,
"the two boxes must tile the band at control width {width}"
);
assert!(
column.x + column.width as i32 <= sb.row_band().x + sb.row_band().width as i32,
"the button column must stay inside the band at control width {width}"
);
}
}
#[test]
fn an_rtl_spin_box_mirrors_its_step_column_and_its_value() {
use crate::core::TextDirection;
use crate::widget::svg::render_to_svg;
let _theme_guard = crate::style::theme_test_guard();
let band = |sb: &SpinBox| sb.row_band();
let mut ltr = SpinBox::new(Rect::new(0, 0, 240, 120));
ltr.set_value(12);
let mut rtl = SpinBox::new(Rect::new(0, 0, 240, 120));
rtl.set_value(12);
rtl.set_direction(TextDirection::RightToLeft);
assert_eq!(ltr.direction(), TextDirection::LeftToRight, "left-to-right by default");
assert_eq!(rtl.direction(), TextDirection::RightToLeft, "and it reports what was set");
let ltr_band = band(<r);
let rtl_band = band(&rtl);
assert_eq!(
ltr.button_column().x + ltr.button_column().width as i32,
ltr_band.x + ltr_band.width as i32,
"in LTR the column ends at the band's right edge"
);
assert_eq!(rtl.button_column().x, rtl_band.x, "in RTL it starts at the band's left edge");
for (sb, label) in [(<r, "ltr"), (&rtl, "rtl")] {
let editable = sb.editable_rect();
let column = sb.button_column();
assert_eq!(
editable.width + column.width,
sb.row_band().width,
"the two boxes must tile the band in {label}"
);
let (left, right) =
if editable.x < column.x { (editable, column) } else { (column, editable) };
assert_eq!(
left.x + left.width as i32,
right.x,
"the two boxes must share an edge in {label}"
);
}
assert!(
rtl.editable_rect().x > ltr.editable_rect().x,
"the value yields to the column, so in RTL its box starts further right: ltr={} rtl={}",
ltr.editable_rect().x,
rtl.editable_rect().x
);
let ltr_svg = render_to_svg(&mut ltr);
let rtl_svg = render_to_svg(&mut rtl);
assert_ne!(
ltr_svg, rtl_svg,
"an RTL spin box must not paint the same picture as an LTR one"
);
}
#[test]
fn a_band_narrower_than_the_step_column_keeps_both_inside_it() {
let width = 30u32;
let sb = SpinBox::new(Rect::new(0, 0, width, 120));
let band = sb.row_band();
let value = sb.editable_rect();
let column = sb.button_column();
for (label, rect) in [("value", value), ("step column", column)] {
assert!(
rect.x >= band.x && rect.x + rect.width as i32 <= band.x + band.width as i32,
"the {label} must stay inside the band: {rect:?} in {band:?}"
);
assert!(rect.width > 0, "and neither is dropped: the {label} is {rect:?}");
}
assert_eq!(value.x + value.width as i32, column.x, "the two boxes share an edge");
assert_eq!(value.width + column.width, width, "and they account for the band");
}
#[test]
fn the_step_buttons_tile_their_column() {
for height in [0u32, 10, 48, 120] {
let sb = SpinBox::new(Rect::new(0, 0, 200, height));
let up = sb.up_button();
let down = sb.down_button();
assert_eq!(up.x, down.x, "both buttons occupy the one column at height {height}");
assert_eq!(up.width, down.width);
assert_eq!(
up.y + up.height as i32,
down.y,
"the up button must end where the down button begins at height {height}"
);
assert_eq!(
up.height + down.height,
sb.button_column().height,
"the two buttons must tile the column at height {height}"
);
}
}
#[test]
fn a_press_on_each_half_steps_in_the_direction_that_is_painted() {
let rect = Rect::new(0, 0, 200, 120);
let top = SpinBox::new(rect).up_button();
let bottom = SpinBox::new(rect).down_button();
let mut sb = SpinBox::new(rect);
sb.set_value(50);
sb.handle_event(&Event::MousePress { pos: Point::new(top.x + 1, top.y + 1), button: 1 });
assert_eq!(sb.value(), 51, "the upper half of the column must step up");
sb.handle_event(&Event::MousePress {
pos: Point::new(bottom.x + 1, bottom.y + 1),
button: 1,
});
assert_eq!(sb.value(), 50, "the lower half of the column must step down");
}
#[test]
fn a_wider_step_column_pushes_the_value_box() {
let width = 240u32;
let band_width = ControlMetrics::full_width_band(
Rect::new(0, 0, width, 120),
dimensions::TEXT_FIELD_MIN_HEIGHT,
)
.width;
let column = SPIN_BOX_BUTTON_WIDTH * SPIN_BOX_BUTTONS;
let sb = SpinBox::new(Rect::new(0, 0, width, 120));
let editable = sb.editable_rect();
assert_eq!(
editable.width,
band_width - column,
"the value box is the band minus the column, not minus a constant from its own edge"
);
assert_eq!(
editable.width + sb.button_column().width,
band_width,
"the two boxes consume the band between them"
);
}
#[test]
fn a_press_inside_the_value_box_does_not_step() {
let mut sb = SpinBox::new(Rect::new(0, 0, 200, 120));
sb.set_value(50);
let editable = sb.editable_rect();
sb.handle_event(&Event::MousePress {
pos: Point::new(editable.x + 1, editable.y + editable.height as i32 / 2),
button: 1,
});
assert_eq!(sb.value(), 50, "the text area is where the user types, not where they step");
}
#[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");
}
}