diff --git a/site/src/content/docs/using/word-prediction.md b/site/src/content/docs/using/word-prediction.md index ea3a2a24..1c0378bb 100644 --- a/site/src/content/docs/using/word-prediction.md +++ b/site/src/content/docs/using/word-prediction.md @@ -17,7 +17,7 @@ phrases follow once the on-device model has started. Only slots with a suggestion are shown. An empty slot leaves a gap, so the other suggestions stay in the same place and the scan skips the gap. -When there are no suggestions at all, the row says why instead, for example **Type for suggestions**, **No suggestions** or **Prediction loading**. The scan skips the row. +When there are no suggestions at all, the row stays blank and the scan skips it. Nothing is written across the keyboard. If prediction fails, a **Retry predictions** key appears on the keyboard; typing still works. Suggestions only change when they need to, so the scan does not restart under you. diff --git a/src-tauri/src/point_workflow.rs b/src-tauri/src/point_workflow.rs index 03346ee7..b171f6e6 100644 --- a/src-tauri/src/point_workflow.rs +++ b/src-tauri/src/point_workflow.rs @@ -3976,7 +3976,6 @@ mod tests { !matches!( style.role, crate::scanning::TileRole::Panel - | crate::scanning::TileRole::Status | crate::scanning::TileRole::MenuBar | crate::scanning::TileRole::Title ) @@ -4327,9 +4326,6 @@ mod tests { // The message is shown clear of the panel, which has no status line. let frame = w.frame(); assert!(frame.tiles.iter().all(|tile| !tile.selected)); - assert!(frame.tiles.iter().all(|tile| { - tile.style.map(|style| style.role) != Some(crate::scanning::TileRole::Status) - })); let label = frame.label.unwrap(); assert_eq!(label.text, format!("{message}\nSelect to return")); assert!(label.hud.is_some()); diff --git a/src-tauri/src/prediction/mod.rs b/src-tauri/src/prediction/mod.rs index c94e170f..fe4a1abf 100644 --- a/src-tauri/src/prediction/mod.rs +++ b/src-tauri/src/prediction/mod.rs @@ -566,7 +566,6 @@ impl Service { status == database::Status::Unavailable, ); keyboard.prediction_loading = status == database::Status::Loading; - keyboard.prediction_tracking(tracking); } /// A reply missed its deadline. One stall should not cost a switch user a /// trip to Retry, so the worker and its private text context are killed @@ -834,7 +833,6 @@ pub fn poll(app: &AppHandle, keyboard: Option<&mut Keyboard>, enabled: bool, ign SERVICE.with(|slot| { let mut s = slot.borrow_mut(); s.observe(activity::snapshot()); - keyboard.prediction_held = s.held; let case = ( keyboard.prediction_shift(), keyboard.caps, diff --git a/src-tauri/src/scan_keyboard.rs b/src-tauri/src/scan_keyboard.rs index 905a5d85..d703caf9 100644 --- a/src-tauri/src/scan_keyboard.rs +++ b/src-tauri/src/scan_keyboard.rs @@ -287,9 +287,6 @@ pub struct Keyboard { prediction_enabled: bool, prediction_failed: bool, pub(crate) prediction_loading: bool, - /// Suggestions wait for the next word after context was lost inside one. - pub(crate) prediction_held: bool, - prediction_tracking: bool, predictions: Option, queued_predictions: Option, prefer_predictions: bool, @@ -341,8 +338,6 @@ impl Keyboard { prediction_enabled: false, prediction_failed: false, prediction_loading: false, - prediction_held: false, - prediction_tracking: true, predictions: None, queued_predictions: None, prefer_predictions: true, @@ -468,38 +463,9 @@ impl Keyboard { } } } - pub fn prediction_tracking(&mut self, tracking: bool) { - self.prediction_tracking = tracking; - } - fn prediction_note(&self) -> Option { - if !self.prediction_enabled { - return None; - } - Some(if self.prediction_failed { - "Prediction unavailable".into() - } else if self.prediction_loading { - "Prediction loading".into() - } else if !self.prediction_tracking { - "Prediction paused".into() - } else if self.prediction_held { - "Suggestions resume next word".into() - } else if self.queued_predictions.is_some() { - "Suggestions updating".into() - } else { - match self.predictions.as_ref() { - Some(batch) if batch.words.iter().flatten().count() == 1 => "1 suggestion".into(), - Some(batch) if batch.words.iter().any(Option::is_some) => { - format!("{} suggestions", batch.words.iter().flatten().count()) - } - Some(_) => "No suggestions".into(), - None => "Type for suggestions".into(), - } - }) - } fn retry_predictions(&mut self) { self.prediction_failed = false; self.prediction_loading = true; - self.prediction_tracking = true; self.predictions = None; self.queued_predictions = None; self.rebuild_rows(); @@ -1082,10 +1048,6 @@ impl Keyboard { } else { None }; - let empty_suggestions = self.rows.first().is_some_and(|row| { - row.iter() - .all(|&key| matches!(key, Key::Prediction(_)) && self.label(key).is_empty()) - }); crate::scan_panel::Panel { key_width: if self.positioning { 128.0 } else { 68.0 }, size: self.scan.options.panel_size, @@ -1112,11 +1074,6 @@ impl Keyboard { }) .collect(), message, - // With no suggestions to show, their row says why instead. - row_label: empty_suggestions - .then(|| self.prediction_note()) - .flatten() - .map(|note| (0, note)), dock: self.dock, selected: (repeating.is_none() && !self.scan.suspended && !self.disabled()) .then(|| self.scan.highlight(&self.rows)), @@ -1250,7 +1207,7 @@ mod tests { } #[test] - fn prediction_badge_explains_empty_slots_without_replacing_scan_prompt() { + fn empty_slots_draw_nothing_and_never_replace_the_scan_prompt() { let mut keyboard = Keyboard::new(false); keyboard.enable_predictions(true); let screen = Rect { @@ -1259,25 +1216,23 @@ mod tests { width: 1920.0, height: 1080.0, }; - // The note is drawn across the suggestion row only while that row is - // empty; it never floats as a message and never sits beside the keys. - let badge = |keyboard: &Keyboard| { + // Nothing is written across the keyboard while the suggestion row is + // empty: no note, no floating message, and the reserved slots stay + // hidden so the other keys never move. + let silent = |keyboard: &Keyboard| { let frame = keyboard.frame(screen, 1.0, ScannerColor::default()); assert!(frame.label.is_none()); - let last = frame.tiles.last().unwrap(); - (last.style.map(|style| style.role) == Some(crate::scanning::TileRole::Status)).then( - || { - assert!(!last.selected); - assert!( - last.rect.y > frame.tiles[0].rect.y + crate::scan_panel::MENU_BAR_HEIGHT - ); - last.text.clone() - }, - ) + assert!(frame.tiles.iter().all(|tile| !tile + .text + .to_lowercase() + .contains("suggestion") + && !tile.text.starts_with("Prediction"))); + frame }; - assert_eq!(badge(&keyboard), Some("Prediction loading".into())); + let keys = |keyboard: &Keyboard| silent(keyboard).tiles.len(); + let loading = keys(&keyboard); keyboard.predictions(None, false); - assert_eq!(badge(&keyboard), Some("Type for suggestions".into())); + assert_eq!(keys(&keyboard), loading); keyboard.predictions( Some(crate::prediction::worker::Batch { refined: false, @@ -1286,7 +1241,7 @@ mod tests { }), false, ); - assert_eq!(badge(&keyboard), Some("No suggestions".into())); + assert_eq!(keys(&keyboard), loading); keyboard.predictions( Some(crate::prediction::worker::Batch { refined: false, @@ -1295,9 +1250,13 @@ mod tests { }), false, ); - assert_eq!(badge(&keyboard), None, "suggestions speak for themselves"); - keyboard.prediction_tracking(false); - assert_eq!(badge(&keyboard), None); + let frame = keyboard.frame(screen, 1.0, ScannerColor::default()); + assert!(frame.tiles.iter().any(|tile| tile.text == "water")); + assert_eq!( + frame.tiles.len(), + loading + 1, + "one suggestion fills one slot" + ); } #[test] @@ -1307,10 +1266,6 @@ mod tests { keyboard.scan.suspended = true; keyboard.predictions(None, true); assert!(keyboard.suspended()); - assert_eq!( - keyboard.prediction_note().as_deref(), - Some("Prediction unavailable") - ); assert_eq!( keyboard.rows.last().unwrap().last(), Some(&Key::RetryPrediction) @@ -1319,10 +1274,8 @@ mod tests { keyboard.choose(Key::RetryPrediction), Some(Output::RetryPrediction) ); - assert_eq!( - keyboard.prediction_note().as_deref(), - Some("Prediction loading") - ); + assert!(keyboard.prediction_loading); + assert!(!keyboard.prediction_failed); assert!(!keyboard .rows .last() @@ -2578,10 +2531,10 @@ mod tests { ); let before = keyboard.frame(screen, 1.0, ScannerColor::default()); assert_eq!(empty.tiles[0].rect, before.tiles[0].rect); - // The empty row carries one label in place of its slots. + // The empty row draws nothing in place of its slots. assert_eq!( before.tiles.len() - empty.tiles.len(), - crate::prediction::SLOTS - 1 + crate::prediction::SLOTS ); keyboard.predictions( Some(crate::prediction::worker::Batch { @@ -2696,28 +2649,7 @@ mod tests { } } #[test] - fn a_hold_is_explained_unless_something_more_pressing_is() { - let mut k = Keyboard::new(true); - k.enable_predictions(true); - k.predictions(None, false); - k.prediction_held = true; - assert_eq!( - k.prediction_note().as_deref(), - Some("Suggestions resume next word") - ); - k.prediction_loading = true; - assert_eq!(k.prediction_note().as_deref(), Some("Prediction loading")); - k.predictions(None, true); - assert_eq!( - k.prediction_note().as_deref(), - Some("Prediction unavailable") - ); - k.predictions(None, false); - k.prediction_held = false; - assert_eq!(k.prediction_note().as_deref(), Some("Type for suggestions")); - } - #[test] - fn an_empty_suggestion_row_says_why() { + fn an_empty_suggestion_row_stays_blank() { let screen = Rect { x: 0.0, y: 0.0, @@ -2728,22 +2660,18 @@ mod tests { k.enable_predictions(true); k.predictions(None, false); let frame = k.frame(screen, 1.0, ScannerColor::Blue); - let labels = frame + // The row under the menu bar holds nothing: no note, no key, and + // no floating message either. + let background = frame.tiles[0].rect; + let first_row_top = background.y + crate::scan_panel::MENU_BAR_HEIGHT; + assert!(frame .tiles .iter() - .filter(|tile| tile.text == "Type for suggestions") - .collect::>(); - // Across the row under the menu bar, never highlighted, and not a - // floating message. - let [label] = labels.as_slice() else { - panic!("expected one label, found {}", labels.len()); - }; - assert!(!label.selected); - let background = frame.tiles[0].rect; - assert!(label.rect.y > background.y + crate::scan_panel::MENU_BAR_HEIGHT); - assert!(label.rect.width > background.width * 0.9); + .skip(1) + .filter(|tile| !tile.is_menu_bar() && tile.text != crate::scan_panel::MENU_BAR_TITLE) + .all(|tile| tile.rect.y > first_row_top + 40.0)); assert!(frame.label.is_none()); - // An empty batch says so too. + // An empty batch is just as quiet. k.predictions( Some(crate::prediction::worker::Batch { refined: true, @@ -2753,7 +2681,7 @@ mod tests { false, ); let frame = k.frame(screen, 1.0, ScannerColor::Blue); - assert!(frame.tiles.iter().any(|tile| tile.text == "No suggestions")); + assert!(!frame.tiles.iter().any(|tile| tile.text == "No suggestions")); // With suggestions, the row shows them and nothing counts them. k.restart(); k.handle(Action::Next); diff --git a/src-tauri/src/scan_menu.rs b/src-tauri/src/scan_menu.rs index a848493d..fba596d3 100644 --- a/src-tauri/src/scan_menu.rs +++ b/src-tauri/src/scan_menu.rs @@ -217,7 +217,6 @@ impl Menu { // Nothing can be chosen while an error or pause waits for Select. selected: (!failed && !self.suspended()).then(|| self.scan.highlight(&self.rows)), row_scan: self.scan.row_scan() || self.scan.group_scan() || escaping, - row_label: None, thickness: options.thickness, appearance: options.panel_theme.appearance(), } diff --git a/src-tauri/src/scan_mouse.rs b/src-tauri/src/scan_mouse.rs index 691df336..bdcda7e0 100644 --- a/src-tauri/src/scan_mouse.rs +++ b/src-tauri/src/scan_mouse.rs @@ -303,7 +303,6 @@ impl MousePanel { dock: self.dock, selected: scanning.then(|| self.scan.highlight(&self.rows)), row_scan: highlight_row, - row_label: None, thickness: self.scan.options.thickness, appearance: self.scan.options.panel_theme.appearance(), } @@ -643,10 +642,6 @@ mod tests { frame.label.is_none(), "nothing to say while scanning normally" ); - assert!(frame - .tiles - .iter() - .all(|tile| tile.style.map(|style| style.role) != Some(TileRole::Status))); let close = frame.tiles.last().unwrap(); assert_eq!(close.text, "Switch to Point"); assert_eq!(close.style.unwrap().role, TileRole::Toolbar); diff --git a/src-tauri/src/scan_panel.rs b/src-tauri/src/scan_panel.rs index 53a1ef78..75e37d69 100644 --- a/src-tauri/src/scan_panel.rs +++ b/src-tauri/src/scan_panel.rs @@ -237,9 +237,6 @@ pub struct Panel { /// key may be chosen. pub selected: Option<(usize, Option>)>, pub row_scan: bool, - /// Passive text drawn across a row, such as an empty suggestion row whose - /// keys are all hidden; never a scan target. - pub row_label: Option<(usize, String)>, pub thickness: Thickness, pub appearance: Appearance, } @@ -373,27 +370,6 @@ impl Panel { c += 1; } } - if let Some((row, text)) = &self.row_label { - frame.tiles.push(FrameTile { - thickness: self.thickness, - color, - appearance: self.appearance, - text: text.clone(), - style: Some(TileStyle { - role: TileRole::Status, - active: false, - row_scan: false, - }), - rect: Rect { - x, - y: y + *row as f64 * row_height, - width, - height: (row_height - gap).max(1.0), - }, - scale, - selected: false, - }); - } if let Some(message) = &self.message { frame.label = Some(message_label(message, frame.tiles[0].rect, screen, scale)); } @@ -467,7 +443,6 @@ mod tests { dock: Dock::default(), selected: Some((0, Some(2..3))), row_scan: false, - row_label: None, thickness: Thickness::Thick, appearance: Appearance::Dark, } @@ -500,7 +475,6 @@ mod tests { dock: Dock::default(), selected: Some((1, None)), row_scan: true, - row_label: None, thickness: Thickness::Thick, appearance: Appearance::Dark, }; @@ -569,7 +543,6 @@ mod tests { dock: Dock { column: 1, row: 0 }, selected: Some((0, None)), row_scan: true, - row_label: None, thickness: Thickness::default(), appearance: Appearance::Dark, }; @@ -613,7 +586,6 @@ mod tests { dock: Dock::default(), selected: None, row_scan: false, - row_label: None, thickness: Thickness::default(), appearance: Appearance::Dark, } @@ -771,7 +743,6 @@ mod tests { dock: Dock::default(), selected: None, row_scan: false, - row_label: None, thickness: Thickness::default(), appearance: Appearance::Dark, } @@ -804,7 +775,6 @@ mod tests { dock, selected: None, row_scan: false, - row_label: None, thickness: Thickness::default(), appearance: Appearance::Dark, }; diff --git a/src-tauri/src/scan_tile.rs b/src-tauri/src/scan_tile.rs index 46cd6772..350025c4 100644 --- a/src-tauri/src/scan_tile.rs +++ b/src-tauri/src/scan_tile.rs @@ -140,7 +140,6 @@ pub fn keyboard_font_size(tile: &FrameTile) -> f64 { { 24.0 } - Some(crate::scanning::TileRole::Status) => 17.0, Some(crate::scanning::TileRole::MenuBar | crate::scanning::TileRole::Title) => 14.0, _ => 16.0, } diff --git a/src-tauri/src/scanning.rs b/src-tauri/src/scanning.rs index c4537c31..ec43d21d 100644 --- a/src-tauri/src/scanning.rs +++ b/src-tauri/src/scanning.rs @@ -262,7 +262,6 @@ pub enum TileRole { Character, Utility, Toolbar, - Status, /// The Settings item on the bar along the top of a panel. It is clicked /// with the mouse, never scanned. MenuBar, diff --git a/src/settings/ScannerPreferences.tsx b/src/settings/ScannerPreferences.tsx index 855a21af..c596d3a2 100644 --- a/src/settings/ScannerPreferences.tsx +++ b/src/settings/ScannerPreferences.tsx @@ -208,7 +208,7 @@ export function ScannerPreferences({ controller, settings: appSettings, onSettin } {area === 'keyboard' && update('wordPrediction', value)} /> -

Uses an offline English word database and an on-device language model. Suggestions appear as you type and after a space. The keyboard shows when suggestions are loading, unavailable, or have no match. If prediction fails, choose Retry predictions on the keyboard; typing still works. Your typing never leaves this computer.

+

Uses an offline English word database and an on-device language model. Suggestions appear as you type and after a space. If prediction fails, a Retry predictions key appears on the keyboard; typing still works. Your typing never leaves this computer.

{appSettings && onSettingsChange && }
}