1use crate::{
17 grid::GridCell,
18 kernel::{Reading, TitleBlock},
19 shell::{glass, mono},
20};
21use blockworx_paint::Zoom;
22
23#[derive(Clone, PartialEq, Debug)]
26pub enum Says {
27 Confirmed(String),
30 Instruction(std::borrow::Cow<'static, str>),
32 Selection(String),
35 Idle {
38 zoom: Zoom,
39 cursor: Option<GridCell>,
40 },
41}
42
43fn title_line(title: &TitleBlock) -> String {
47 let head = format!("{} {SEPARATOR} rev {}", title.author, title.rev.get());
48 match title.written.trim() {
49 "" => head,
50 when => format!("{head} {SEPARATOR} {when}"),
51 }
52}
53
54const SEPARATOR: char = '\u{00b7}';
57
58impl Says {
59 fn of(confirmed: Option<String>, reading: Reading) -> Says {
65 let Reading {
66 tool,
67 selection,
68 zoom,
69 cursor,
70 title: _,
73 } = reading;
74 if let Some(said) = confirmed {
75 return Says::Confirmed(said);
76 }
77 if let Some(instruction) = tool {
78 return Says::Instruction(instruction);
79 }
80 if let Some(path) = selection {
81 return Says::Selection(path);
82 }
83 Says::Idle { zoom, cursor }
84 }
85}
86
87pub fn say(ctx: &egui::Context, said: impl Into<String>) {
90 let raised = ctx.input(|i| i.time);
91 ctx.data_mut(|data| {
92 data.insert_temp(
93 id(),
94 Confirmation {
95 said: said.into(),
96 raised,
97 },
98 );
99 });
100 ctx.request_repaint();
101}
102
103pub fn status_line(chrome: &mut super::Chrome, reading: Reading) {
105 let title = reading.title.clone();
106 let says = Says::of(confirmed(chrome.ctx()), reading);
107 chrome.piece(glass::Berth::StatusLine, |ui| {
108 ui.vertical(|ui| {
109 ui.horizontal(|ui| {
110 ui.spacing_mut().item_spacing.x = GAP;
111 match &says {
112 Says::Confirmed(said) => {
115 let ink = glass::full_ink(ui.visuals());
116 ui.label(egui::RichText::new(said).color(ink));
117 }
118 Says::Instruction(instruction) => {
119 ui.label(instruction.as_ref());
120 }
121 Says::Selection(path) => {
122 ui.label(egui::RichText::new(path).weak());
123 }
124 Says::Idle { zoom, cursor } => {
125 let percent = (zoom.get() * 100.0).round() as i32;
126 ui.label(mono(format!("{percent}%")).weak());
127 ui.label(
132 mono(match cursor {
133 Some(cell) => format!("{cell}"),
134 None => "\u{2013},\u{2013}".to_owned(),
135 })
136 .weak(),
137 );
138 }
139 }
140 });
141 ui.label(egui::RichText::new(title_line(&title)).weak());
145 });
146 });
147}
148
149fn confirmed(ctx: &egui::Context) -> Option<String> {
155 let showing = ctx.data(|data| data.get_temp::<Confirmation>(id()))?;
156 let elapsed =
157 core::time::Duration::from_secs_f64((ctx.input(|i| i.time) - showing.raised).max(0.0));
158 let Some(left) = DWELL.checked_sub(elapsed) else {
159 ctx.data_mut(|data| data.remove::<Confirmation>(id()));
160 return None;
161 };
162 ctx.request_repaint_after(left);
163 Some(showing.said)
164}
165
166#[derive(Clone)]
170struct Confirmation {
171 said: String,
172 raised: f64,
173}
174
175#[cfg(test)]
178pub(crate) fn showing(ctx: &egui::Context) -> Option<String> {
179 ctx.data(|data| data.get_temp::<Confirmation>(id()))
180 .map(|showing| showing.said)
181}
182
183fn id() -> egui::Id {
184 egui::Id::new("shell_status_confirmation")
185}
186
187const DWELL: core::time::Duration = core::time::Duration::from_secs(2);
189
190const GAP: f32 = 16.0;
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::canvas::convert::IntoGeom as _;
197 use crate::panels::painted::Chrome;
198 use crate::shell::tests::screen;
199
200 fn reading() -> Reading {
201 Reading {
202 tool: None,
203 selection: None,
204 zoom: Zoom::unity(),
205 cursor: Some(GridCell { x: 12, y: -4 }),
206 title: title(),
207 }
208 }
209
210 fn title() -> TitleBlock {
211 TitleBlock {
212 author: AUTHOR.to_owned(),
213 rev: blockworx_doc::fixtures::rev(4),
214 written: WRITTEN.to_owned(),
215 }
216 }
217
218 const AUTHOR: &str = "Ada Lovelace";
219 const WRITTEN: &str = "2026-09-01 14:32";
220
221 #[test]
225 fn the_line_carries_a_title_block_under_whatever_it_is_saying() {
226 let mut chrome = Chrome::new(screen().geom());
227 let show = |ui: &mut egui::Ui| {
228 let mut floating = crate::shell::Chrome::over(ui.ctx(), ui.max_rect());
229 status_line(&mut floating, reading());
230 };
231 chrome.settle(show);
232 let block = format!("{AUTHOR} \u{00b7} rev 4 \u{00b7} {WRITTEN}");
233 assert!(
234 chrome.shows(&block),
235 "the title block never drew: {:?}",
236 chrome.texts(),
237 );
238 let state = chrome
239 .rect("100%")
240 .expect("the state line lost its zoom readout");
241 let title = chrome.rect(&block).expect("the title block never drew");
242 assert!(
243 title.top() >= state.bottom(),
244 "the title block is not the second line: {title:?} against {state:?}",
245 );
246
247 chrome.settle(|ui| {
249 say(ui.ctx(), CONFIRMED);
250 let mut floating = crate::shell::Chrome::over(ui.ctx(), ui.max_rect());
251 status_line(&mut floating, reading());
252 });
253 assert!(
254 chrome.shows(CONFIRMED) && chrome.shows(&block),
255 "a confirmation covered the title block: {:?}",
256 chrome.texts(),
257 );
258 }
259
260 #[test]
265 fn a_confirmation_is_painted_in_ink_the_canvas_can_be_read_against() {
266 let visuals = crate::canvas::convert::visuals(
267 &blockworx_paint::Scheme::default().palette(blockworx_paint::Luminance::Dark),
268 );
269 let mut chrome = Chrome::new(screen().geom());
270 chrome.ctx().set_visuals(visuals.clone());
271 chrome.settle(|ui| {
272 say(ui.ctx(), CONFIRMED);
273 let mut floating = crate::shell::Chrome::over(ui.ctx(), ui.max_rect());
274 status_line(&mut floating, reading());
275 });
276 let ink = chrome
277 .format_of(CONFIRMED)
278 .expect("the confirmation never drew")
279 .color;
280 assert_eq!(ink, glass::full_ink(&visuals));
281 assert_ne!(
282 ink,
283 visuals.strong_text_color(),
284 "the confirmation went back to egui's strong text colour",
285 );
286 }
287
288 #[test]
291 fn a_session_with_no_clock_says_what_it_knows_and_no_more() {
292 assert_eq!(
293 title_line(&TitleBlock {
294 written: String::new(),
295 ..title()
296 }),
297 format!("{AUTHOR} \u{00b7} rev 4"),
298 );
299 assert_eq!(
300 title_line(&title()),
301 format!("{AUTHOR} \u{00b7} rev 4 \u{00b7} {WRITTEN}"),
302 );
303 }
304
305 #[test]
308 fn the_room_the_line_keeps_clears_both_of_its_lines() {
309 let mut chrome = Chrome::new(screen().geom());
310 let mut region = egui::Rect::NOTHING;
311 chrome.settle(|ui| {
312 let mut floating = crate::shell::Chrome::over(ui.ctx(), ui.max_rect());
313 status_line(&mut floating, reading());
314 region = floating.safe().region();
315 });
316 let state = chrome
317 .rect("100%")
318 .expect("the state line lost its zoom readout");
319 let block = format!("{AUTHOR} \u{00b7} rev 4 \u{00b7} {WRITTEN}");
320 let title = chrome.rect(&block).expect("the title block never drew");
321 assert!(
322 title.top() >= state.bottom(),
323 "precondition: the title block is the second line",
324 );
325 assert!(
326 region.bottom() <= state.top(),
327 "the canvas may land on the status line: {region:?} against {state:?}",
328 );
329 }
330
331 #[test]
334 fn the_line_draws_no_container_and_answers_no_pointer() {
335 let mut chrome = Chrome::new(screen().geom());
336 chrome.settle(|ui| {
337 let mut floating = crate::shell::Chrome::over(ui.ctx(), ui.max_rect());
338 status_line(&mut floating, reading());
339 });
340 let at = crate::shell::berth_rect(chrome.ctx(), glass::Berth::StatusLine)
341 .expect("the status line never laid out");
342 assert!(
343 !crate::shell::over_the_chrome(chrome.ctx(), at.center()),
344 "the status line took the canvas's pointer at {at:?}",
345 );
346 assert!(
347 chrome.shows("100%"),
348 "the idle line lost its zoom: {:?}",
349 chrome.texts(),
350 );
351 }
352
353 #[test]
357 fn a_confirmation_reverts_after_its_dwell_and_settles() {
358 let mut said = false;
359 let settle = crate::canvas::settle::probe(200, |ui| {
360 if !said {
361 say(ui.ctx(), CONFIRMED);
362 said = true;
363 }
364 let mut floating = crate::shell::Chrome::over(ui.ctx(), ui.max_rect());
365 status_line(&mut floating, reading());
366 });
367 crate::canvas::settle::assert_settles(&settle, 190);
368 }
369
370 #[test]
373 fn the_confirmation_is_painted_and_then_is_not() {
374 let mut chrome = Chrome::new(screen().geom());
375 let show = |ui: &mut egui::Ui| {
376 let mut floating = crate::shell::Chrome::over(ui.ctx(), ui.max_rect());
377 status_line(&mut floating, reading());
378 };
379 chrome.settle(|ui| {
380 say(ui.ctx(), CONFIRMED);
381 show(ui);
382 });
383 assert!(
384 chrome.shows(CONFIRMED),
385 "the confirmation never drew: {:?}",
386 chrome.texts(),
387 );
388 chrome.wait(core::time::Duration::from_millis(2_500), show);
389 assert!(
390 !chrome.shows(CONFIRMED),
391 "the confirmation outstayed its dwell: {:?}",
392 chrome.texts(),
393 );
394 assert!(chrome.shows("100%"), "the line never came back");
395 }
396
397 const CONFIRMED: &str = "Add block Filter \u{2014} rev 4";
398}