blockworx_web/
status_line.rs1use core::time::Duration;
16
17use blockworx_kernel::Reading;
18use dioxus::prelude::*;
19use dioxus::web::WebEventExt as _;
20
21use crate::shell::{Band, Owed, Shell, after, measured};
22
23const DWELL: Duration = Duration::from_secs(2);
25
26#[derive(Clone, PartialEq, Eq, Debug)]
29pub enum Says {
30 Confirmed(String),
32 Instruction(String),
34 Selection(String),
36 Idle {
37 zoom: String,
38 cursor: String,
39 },
40}
41
42impl Says {
43 #[must_use]
45 pub fn of(confirmed: Option<&str>, reading: &Reading) -> Self {
46 if let Some(said) = confirmed {
47 return Says::Confirmed(said.to_owned());
48 }
49 if let Some(how) = &reading.tool {
50 return Says::Instruction(how.to_string());
51 }
52 if let Some(what) = &reading.selection {
53 return Says::Selection(what.clone());
54 }
55 Says::Idle {
56 zoom: format!("{:.0}%", reading.zoom.get() * 100.0),
57 cursor: reading
58 .cursor
59 .map_or_else(|| "\u{2013},\u{2013}".to_owned(), |cell| format!("{cell}")),
60 }
61 }
62}
63
64#[must_use]
69pub fn writing(owed: Owed) -> Option<String> {
70 match owed {
71 Owed(0) => None,
72 Owed(depth) => Some(format!("Writing\u{2026} ({depth})")),
73 }
74}
75
76#[component]
77pub fn StatusLine(shell: Shell) -> Element {
78 let chrome = shell.chrome();
79 let owed = shell.owed();
80 let mut confirmed = use_signal(|| None::<String>);
81 let mut holding = use_signal(|| 0_u64);
82 use_effect(move || {
83 let Some(landed) = chrome.read().landed.clone() else {
84 return;
85 };
86 let taken = *holding.peek() + 1;
87 holding.set(taken);
88 confirmed.set(Some(landed));
89 after(DWELL, move || {
90 if *holding.peek() == taken {
91 confirmed.set(None);
92 }
93 });
94 });
95
96 let read = chrome.read();
97 let says = Says::of(confirmed.read().as_deref(), &read.status);
98 let block = read.status.title.clone();
99 let owes = writing(owed());
100 let written = if block.written.is_empty() {
101 String::new()
102 } else {
103 format!(" \u{00b7} {}", block.written)
104 };
105 let shell = use_hook(|| CopyValue::new(shell));
106 rsx! {
107 div {
108 class: "pointer-events-none absolute bottom-0 right-0 z-10 max-w-[30%] p-4 \
109 text-right text-xs leading-relaxed",
110 role: "status",
111 onmounted: move |event| {
112 if let Some(element) = event.try_as_web_event() {
113 covers(&shell.read(), &element);
114 }
115 },
116 onresize: move |event| {
117 if let Some(entry) = event.try_as_web_event() {
118 covers(&shell.read(), &entry.target());
119 }
120 },
121 div { class: "flex items-center justify-end gap-4",
122 match says {
123 Says::Confirmed(said) => rsx! {
124 span { class: "truncate font-medium text-sky-600 dark:text-sky-400", "{said}" }
125 },
126 Says::Instruction(how) => rsx! {
127 span { class: "truncate text-zinc-600 dark:text-zinc-300", "{how}" }
128 },
129 Says::Selection(what) => rsx! {
130 span { class: "truncate text-zinc-500 dark:text-zinc-400", "{what}" }
131 },
132 Says::Idle { zoom, cursor } => rsx! {
133 span { class: "font-mono text-zinc-500 dark:text-zinc-500", "{zoom}" }
134 span { class: "font-mono text-zinc-500 dark:text-zinc-500", "{cursor}" }
135 },
136 }
137 if let Some(owes) = owes {
138 span { class: "text-amber-600 dark:text-amber-400", "data-owes": "true", "{owes}" }
139 }
140 }
141 div { class: "truncate text-zinc-500 dark:text-zinc-500",
142 "{block.author} \u{00b7} rev {block.rev.get()}{written}"
143 }
144 }
145 }
146}
147
148fn covers(shell: &Shell, element: &web_sys::Element) {
151 if let Some(at) = measured(element) {
152 shell.covers(Band::Status, at);
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use blockworx_geom::grid::GridCell;
159 use blockworx_kernel::TitleBlock;
160 use blockworx_paint::Zoom;
161
162 use super::*;
163
164 fn resting() -> Reading {
165 Reading {
166 tool: None,
167 selection: None,
168 zoom: Zoom::new(1.0),
169 cursor: None,
170 title: TitleBlock {
171 author: "tester".to_owned(),
172 rev: blockworx_doc::rev::Rev::ZERO,
173 written: String::new(),
174 },
175 }
176 }
177
178 #[test]
181 fn the_line_says_the_most_relevant_thing_there_is() {
182 let mut reading = resting();
183 assert!(matches!(Says::of(None, &reading), Says::Idle { .. },),);
184 reading.selection = Some("Rig / Filter".to_owned());
185 assert_eq!(
186 Says::of(None, &reading),
187 Says::Selection("Rig / Filter".to_owned()),
188 );
189 reading.tool = Some("Click one corner, then the other".into());
190 assert_eq!(
191 Says::of(None, &reading),
192 Says::Instruction("Click one corner, then the other".to_owned()),
193 );
194 assert_eq!(
195 Says::of(Some("Added a block"), &reading),
196 Says::Confirmed("Added a block".to_owned()),
197 );
198 }
199
200 #[test]
203 fn idle_names_the_zoom_and_the_cell_under_the_pointer() {
204 let mut reading = resting();
205 reading.zoom = Zoom::new(0.5);
206 let Says::Idle { zoom, cursor } = Says::of(None, &reading) else {
207 panic!("a resting line is idle");
208 };
209 assert_eq!(zoom, "50%");
210 assert_eq!(cursor, "\u{2013},\u{2013}");
211 reading.cursor = Some(GridCell { x: 3, y: 4 });
212 let Says::Idle { cursor, .. } = Says::of(None, &reading) else {
213 panic!("a resting line is idle");
214 };
215 assert_ne!(cursor, "\u{2013},\u{2013}");
216 }
217
218 #[test]
221 fn the_slot_says_what_the_journal_still_owes() {
222 assert_eq!(writing(Owed(0)), None);
223 assert_eq!(writing(Owed(1)).as_deref(), Some("Writing\u{2026} (1)"));
224 assert_eq!(writing(Owed(3)).as_deref(), Some("Writing\u{2026} (3)"));
225 }
226}