Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 12 kB View raw
1use core::str::FromStr; 2 3use super::parsed_input::{ParsedInput, ParsedInputResponse, ParsedValue}; 4 5pub type NumericInput<'state, T> = ParsedInput<'state, T>; 6pub type NumericInputResponse<T> = ParsedInputResponse<T>; 7 8macro_rules! impl_parsed_value_via_fromstr { 9 ($($ty:ty),+ $(,)?) => {$( 10 impl ParsedValue for $ty { 11 type Error = <$ty as FromStr>::Err; 12 13 fn parse(text: &str) -> Result<Self, Self::Error> { 14 text.trim().parse::<$ty>() 15 } 16 } 17 )+}; 18} 19 20impl_parsed_value_via_fromstr!(i8, i16, i32, i64, i128, isize); 21impl_parsed_value_via_fromstr!(u8, u16, u32, u64, u128, usize); 22 23#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] 24pub enum NumericFloatParseError { 25 #[error("not a finite number")] 26 NotFinite, 27 #[error("invalid float: {0}")] 28 Parse(String), 29} 30 31macro_rules! impl_parsed_value_finite_float { 32 ($($ty:ty),+ $(,)?) => {$( 33 impl ParsedValue for $ty { 34 type Error = NumericFloatParseError; 35 36 fn parse(text: &str) -> Result<Self, Self::Error> { 37 let trimmed = text.trim(); 38 let value: $ty = trimmed 39 .parse() 40 .map_err(|_| NumericFloatParseError::Parse(trimmed.to_owned()))?; 41 if value.is_finite() { 42 Ok(value) 43 } else { 44 Err(NumericFloatParseError::NotFinite) 45 } 46 } 47 } 48 )+}; 49} 50 51impl_parsed_value_finite_float!(f32, f64); 52 53#[cfg(test)] 54mod tests { 55 use std::sync::Arc; 56 57 use super::{NumericInput, NumericInputResponse}; 58 use crate::focus::FocusManager; 59 use crate::frame::FrameCtx; 60 use crate::hit_test::{HitFrame, HitState}; 61 use crate::hotkey::HotkeyTable; 62 use crate::input::{FrameInstant, InputSnapshot}; 63 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 64 use crate::strings::StringKey; 65 use crate::strings::StringTable; 66 use crate::theme::Theme; 67 use crate::widget_id::{WidgetId, WidgetKey}; 68 use crate::widgets::parsed_input::{ParsedValue, show_parsed_input}; 69 use crate::widgets::{MemoryClipboard, TextInputState}; 70 71 const PLACEHOLDER: StringKey = StringKey::new("numeric.placeholder"); 72 73 fn rect() -> LayoutRect { 74 LayoutRect::new( 75 LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), 76 LayoutSize::new(LayoutPx::new(120.0), LayoutPx::new(24.0)), 77 ) 78 } 79 80 fn id_widget() -> WidgetId { 81 WidgetId::ROOT.child(WidgetKey::new("numeric")) 82 } 83 84 fn run<T: ParsedValue + 'static>(state: &mut TextInputState) -> NumericInputResponse<T> { 85 let theme = Arc::new(Theme::light()); 86 let mut focus = FocusManager::new(); 87 focus.register_focusable(id_widget()); 88 focus.request_focus(id_widget()); 89 focus.end_frame(); 90 let table = HotkeyTable::new(); 91 let mut hits = HitFrame::new(); 92 let prev = HitState::new(); 93 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 94 input.text_committed = String::new(); 95 let mut clipboard = MemoryClipboard::default(); 96 let widget = NumericInput::<T>::new(id_widget(), rect(), PLACEHOLDER, state); 97 let mut shaper = bone_text::Shaper::new(); 98 let mut a11y = crate::a11y::AccessTreeBuilder::new(); 99 let mut ctx = FrameCtx::new( 100 theme, 101 &mut input, 102 &mut focus, 103 &table, 104 StringTable::empty(), 105 &mut hits, 106 &prev, 107 &mut a11y, 108 &mut shaper, 109 ); 110 show_parsed_input(&mut ctx, widget, &mut clipboard) 111 } 112 113 #[test] 114 fn parses_valid_integer() { 115 let mut state = TextInputState::from_text("42"); 116 let response: NumericInputResponse<i32> = run(&mut state); 117 assert_eq!(response.value, Some(42)); 118 assert!(response.error.is_none()); 119 } 120 121 #[test] 122 fn parses_valid_float() { 123 let mut state = TextInputState::from_text("2.5"); 124 let response: NumericInputResponse<f64> = run(&mut state); 125 assert_eq!(response.value, Some(2.5)); 126 assert!(response.error.is_none()); 127 } 128 129 #[test] 130 fn rejects_garbage_with_typed_error() { 131 let mut state = TextInputState::from_text("not-a-number"); 132 let response: NumericInputResponse<u32> = run(&mut state); 133 assert!(response.value.is_none()); 134 assert!(response.error.is_some()); 135 } 136 137 #[test] 138 fn empty_text_is_neither_value_nor_error() { 139 let mut state = TextInputState::default(); 140 let response: NumericInputResponse<i64> = run(&mut state); 141 assert!(response.value.is_none()); 142 assert!(response.error.is_none()); 143 } 144 145 #[test] 146 fn negative_integer_parses_when_signed() { 147 let mut state = TextInputState::from_text("-7"); 148 let response: NumericInputResponse<i32> = run(&mut state); 149 assert_eq!(response.value, Some(-7)); 150 } 151 152 #[test] 153 fn negative_integer_errors_when_unsigned() { 154 let mut state = TextInputState::from_text("-7"); 155 let response: NumericInputResponse<u32> = run(&mut state); 156 assert!(response.error.is_some()); 157 } 158 159 #[test] 160 fn float_rejects_nan() { 161 use super::NumericFloatParseError; 162 let mut state = TextInputState::from_text("NaN"); 163 let response: NumericInputResponse<f64> = run(&mut state); 164 assert!(response.value.is_none()); 165 assert_eq!(response.error, Some(NumericFloatParseError::NotFinite)); 166 } 167 168 #[test] 169 fn float_rejects_infinity() { 170 use super::NumericFloatParseError; 171 let mut state = TextInputState::from_text("inf"); 172 let response: NumericInputResponse<f64> = run(&mut state); 173 assert!(response.value.is_none()); 174 assert_eq!(response.error, Some(NumericFloatParseError::NotFinite)); 175 } 176 177 #[test] 178 fn float_accepts_finite_value() { 179 let mut state = TextInputState::from_text("2.5"); 180 let response: NumericInputResponse<f64> = run(&mut state); 181 assert_eq!(response.value, Some(2.5)); 182 assert!(response.error.is_none()); 183 } 184 185 #[test] 186 fn whitespace_around_value_parses() { 187 let mut state = TextInputState::from_text(" 12 "); 188 let response: NumericInputResponse<i32> = run(&mut state); 189 assert_eq!(response.value, Some(12)); 190 } 191 192 #[test] 193 fn typing_alone_does_not_commit() { 194 let mut state = TextInputState::from_text("42"); 195 let response: NumericInputResponse<i32> = run(&mut state); 196 assert_eq!(response.value, Some(42)); 197 assert!(response.committed.is_none()); 198 } 199 200 #[test] 201 fn enter_while_focused_commits_value() { 202 use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey}; 203 let mut state = TextInputState::from_text("42"); 204 let theme = Arc::new(Theme::light()); 205 let mut focus = FocusManager::new(); 206 focus.register_focusable(id_widget()); 207 focus.request_focus(id_widget()); 208 focus.end_frame(); 209 let table = HotkeyTable::new(); 210 let mut hits = HitFrame::new(); 211 let prev = HitState::new(); 212 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 213 input.keys_pressed.push(KeyEvent::new( 214 KeyCode::Named(NamedKey::Enter), 215 ModifierMask::NONE, 216 )); 217 let mut clipboard = MemoryClipboard::default(); 218 let widget = NumericInput::<i32>::new(id_widget(), rect(), PLACEHOLDER, &mut state); 219 let response = { 220 let mut shaper = bone_text::Shaper::new(); 221 let mut a11y = crate::a11y::AccessTreeBuilder::new(); 222 let mut ctx = FrameCtx::new( 223 theme, 224 &mut input, 225 &mut focus, 226 &table, 227 StringTable::empty(), 228 &mut hits, 229 &prev, 230 &mut a11y, 231 &mut shaper, 232 ); 233 show_parsed_input(&mut ctx, widget, &mut clipboard) 234 }; 235 assert_eq!(response.committed, Some(42)); 236 assert!(input.keys_pressed.is_empty(), "Enter drained on commit"); 237 } 238 239 #[test] 240 fn focus_loss_commits_value() { 241 let mut state = TextInputState::from_text("42"); 242 state.was_focused = true; 243 let theme = Arc::new(Theme::light()); 244 let mut focus = FocusManager::new(); 245 let table = HotkeyTable::new(); 246 let mut hits = HitFrame::new(); 247 let prev = HitState::new(); 248 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 249 let mut clipboard = MemoryClipboard::default(); 250 let widget = NumericInput::<i32>::new(id_widget(), rect(), PLACEHOLDER, &mut state); 251 let mut shaper = bone_text::Shaper::new(); 252 let mut a11y = crate::a11y::AccessTreeBuilder::new(); 253 let mut ctx = FrameCtx::new( 254 theme, 255 &mut input, 256 &mut focus, 257 &table, 258 StringTable::empty(), 259 &mut hits, 260 &prev, 261 &mut a11y, 262 &mut shaper, 263 ); 264 let response = show_parsed_input(&mut ctx, widget, &mut clipboard); 265 assert_eq!(response.committed, Some(42)); 266 assert!( 267 !state.was_focused, 268 "post-call state reflects current focus, not prior", 269 ); 270 } 271 272 #[test] 273 fn two_frame_focus_loss_commits_value() { 274 let mut state = TextInputState::from_text("42"); 275 let theme = Arc::new(Theme::light()); 276 let table = HotkeyTable::new(); 277 let mut clipboard = MemoryClipboard::default(); 278 279 let mut focus = FocusManager::new(); 280 focus.register_focusable(id_widget()); 281 focus.request_focus(id_widget()); 282 focus.end_frame(); 283 284 { 285 let mut hits = HitFrame::new(); 286 let prev = HitState::new(); 287 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 288 let widget = NumericInput::<i32>::new(id_widget(), rect(), PLACEHOLDER, &mut state); 289 let mut shaper = bone_text::Shaper::new(); 290 let mut a11y = crate::a11y::AccessTreeBuilder::new(); 291 let mut ctx = FrameCtx::new( 292 theme.clone(), 293 &mut input, 294 &mut focus, 295 &table, 296 StringTable::empty(), 297 &mut hits, 298 &prev, 299 &mut a11y, 300 &mut shaper, 301 ); 302 let response = show_parsed_input(&mut ctx, widget, &mut clipboard); 303 assert!( 304 response.committed.is_none(), 305 "frame 1 still focused, no commit yet", 306 ); 307 } 308 309 let mut focus = FocusManager::new(); 310 let mut hits = HitFrame::new(); 311 let prev = HitState::new(); 312 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 313 let widget = NumericInput::<i32>::new(id_widget(), rect(), PLACEHOLDER, &mut state); 314 let mut shaper = bone_text::Shaper::new(); 315 let mut a11y = crate::a11y::AccessTreeBuilder::new(); 316 let mut ctx = FrameCtx::new( 317 theme, 318 &mut input, 319 &mut focus, 320 &table, 321 StringTable::empty(), 322 &mut hits, 323 &prev, 324 &mut a11y, 325 &mut shaper, 326 ); 327 let response = show_parsed_input(&mut ctx, widget, &mut clipboard); 328 assert_eq!( 329 response.committed, 330 Some(42), 331 "frame 2 unfocused after being focused frame 1: commit fires", 332 ); 333 } 334}