1use blockworx_geom::Rect;
9use blockworx_kernel::{FrameRate, NavTree, Notice, Overlay, Reading, TopBar, View};
10use blockworx_paint::{Chord, Cursor, EditField, Vantage};
11use blockworx_store::doc::{Viewing, Writability};
12use blockworx_store::history::Row;
13use blockworx_tools::commands::{BINDINGS, Command, CommandId, CommandSet, Precedence, Rendered};
14use blockworx_tools::names::ToolName;
15
16#[derive(Clone, PartialEq, Debug)]
18pub struct Chrome {
19 pub title: String,
20 pub tool: ToolName,
21 pub top_bar: TopBar,
22 pub status: Reading,
23 pub history: Vec<Row>,
24 pub nav_tree: NavTree,
25 pub overlay: Option<Overlay>,
26 pub notices: Vec<Notice>,
27 pub landed: Option<String>,
28 pub commands: Commands,
29 pub edit_text: Option<EditField>,
31 pub cursor: Option<Cursor>,
32 pub selection_bounds: Option<Rect>,
33 pub vantage: Vantage,
36 pub writable: Writability,
37 pub selected: usize,
38 pub frame_rate: FrameRate,
40}
41
42impl Chrome {
43 #[must_use]
44 pub fn of(view: &View) -> Self {
45 Self {
46 title: view.title.clone(),
47 tool: view.tool,
48 top_bar: view.top_bar.clone(),
49 status: view.status.clone(),
50 history: view.history.clone(),
51 nav_tree: view.nav_tree.clone(),
52 overlay: view.overlay.clone(),
53 notices: view.notices.clone(),
54 landed: view.landed.clone(),
55 commands: Commands::of(&view.commands),
56 edit_text: view.edit_text.clone(),
57 cursor: view.cursor,
58 selection_bounds: view.selection_bounds,
59 vantage: view.vantage,
60 writable: view.writable,
61 selected: view.selected,
62 frame_rate: view.frame_rate,
63 }
64 }
65}
66
67#[derive(Clone, Copy, PartialEq, Eq, Debug)]
69pub enum Availability {
70 Live,
71 Withheld,
74}
75
76impl Availability {
77 fn of(command: &Command) -> Self {
78 if command.withheld() {
79 Self::Withheld
80 } else {
81 Self::Live
82 }
83 }
84
85 #[must_use]
87 pub fn disabled(self) -> bool {
88 self == Self::Withheld
89 }
90}
91
92#[derive(Clone, Copy, PartialEq, Eq, Debug)]
99pub enum Withholding {
100 Lens,
102 ReadOnly,
104 Nothing,
107}
108
109impl Withholding {
110 #[must_use]
113 pub fn of(viewing: Viewing, writable: Writability) -> Self {
114 match (viewing, writable) {
115 (Viewing::Past(_), _) => Withholding::Lens,
116 (_, Writability::ReadOnly) => Withholding::ReadOnly,
117 (_, Writability::Writable) => Withholding::Nothing,
118 }
119 }
120
121 #[must_use]
123 pub fn says(self) -> &'static str {
124 match self {
125 Withholding::Lens => {
126 "not while an earlier rev is on the canvas \u{2014} Return to edit"
127 }
128 Withholding::ReadOnly => "this document was opened without a write lock",
129 Withholding::Nothing => "nothing to do it to",
130 }
131 }
132}
133
134#[derive(Clone, PartialEq, Eq, Debug)]
138pub struct Face {
139 pub id: CommandId,
140 pub label: String,
141 pub availability: Availability,
142 pub precedence: Precedence,
145 pub rendered: Rendered,
149}
150
151impl Face {
152 #[must_use]
154 pub fn live(&self) -> bool {
155 self.availability == Availability::Live
156 }
157}
158
159#[derive(Clone, PartialEq, Eq, Debug, Default)]
161pub struct Commands(Vec<Face>);
162
163impl Commands {
164 #[cfg(test)]
166 #[must_use]
167 pub fn showing(faces: Vec<Face>) -> Self {
168 Self(faces)
169 }
170
171 fn of(set: &CommandSet) -> Self {
172 Self(
173 set.iter_all()
174 .map(|command| Face {
175 id: command.id,
176 label: command.label.to_string(),
177 availability: Availability::of(command),
178 precedence: command.precedence,
179 rendered: command.rendered(),
180 })
181 .collect(),
182 )
183 }
184
185 pub fn drawn(&self) -> impl Iterator<Item = &Face> {
188 self.0
189 .iter()
190 .filter(|face| face.rendered == Rendered::AsAButton)
191 }
192
193 pub fn live(&self) -> impl Iterator<Item = &Face> {
196 self.0.iter().filter(|face| face.live())
197 }
198
199 #[must_use]
201 pub fn face(&self, id: CommandId) -> Option<&Face> {
202 self.0.iter().find(|face| face.id == id)
203 }
204
205 #[must_use]
209 pub fn bound(&self, chord: Chord) -> Option<CommandId> {
210 BINDINGS
211 .iter()
212 .find(|(bound, _)| *bound == chord)
213 .map(|(_, id)| *id)
214 .filter(|id| {
215 self.face(*id)
216 .is_some_and(|face| face.availability == Availability::Live)
217 })
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use blockworx_canvas2d::Glyphs;
224 use blockworx_geom::{Rect, pos2};
225 use blockworx_kernel::{Event, Session, kernel};
226 use blockworx_paint::{FontChoice, Key, Modifiers};
227 use blockworx_store::{doc::Doc, record::Identity};
228
229 use super::*;
230
231 const VIEWPORT: Rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(800.0, 600.0));
232
233 fn opened() -> (Session, Glyphs) {
237 let scene = vec![
238 blockworx_editor::widget::test_fixtures::block_in(
239 1,
240 blockworx_editor::path::Scope::Root,
241 Rect::from_min_max(pos2(40.0, 40.0), pos2(200.0, 160.0)),
242 ),
243 blockworx_editor::widget::test_fixtures::titled(1, "Rig"),
244 ];
245 let commit = blockworx_doc::commit::Commit::new("Built a scene".into(), scene);
246 let repo = blockworx_doc::repo::Repo::folding(&[commit]).expect("the scene folds");
247 (
248 Session::opening(Doc::scratch(repo), Identity::new("tester")),
249 Glyphs::new(FontChoice::default()),
250 )
251 }
252
253 fn viewed(events: Vec<Event>) -> View {
256 let (mut session, glyphs) = opened();
257 let mut batch = vec![Event::Viewport(VIEWPORT)];
258 batch.extend(events);
259 kernel(&mut session, batch, &glyphs)
260 }
261
262 fn chord(key: Key) -> Chord {
263 Chord {
264 modifiers: Modifiers::Command,
265 key,
266 }
267 }
268
269 #[test]
270 fn the_model_is_the_answer_minus_its_diagram() {
271 let view = viewed(Vec::new());
272 let chrome = Chrome::of(&view);
273 assert!(
274 !view.draw_list.is_empty(),
275 "a view with nothing drawn would prove nothing about what is left out",
276 );
277 assert_eq!(chrome.title, view.title);
278 assert_eq!(chrome.tool, view.tool);
279 assert_eq!(chrome.top_bar, view.top_bar);
280 assert_eq!(chrome.status, view.status);
281 assert_eq!(chrome.nav_tree, view.nav_tree);
282 assert_eq!(chrome.vantage, view.vantage);
283 assert_eq!(chrome.cursor, view.cursor);
284 assert_eq!(chrome.selected, view.selected);
285 }
286
287 #[test]
290 fn a_call_that_changed_nothing_answers_the_same_model() {
291 let (mut session, glyphs) = opened();
292 let first = Chrome::of(&kernel(
293 &mut session,
294 vec![Event::Viewport(VIEWPORT)],
295 &glyphs,
296 ));
297 let again = Chrome::of(&kernel(&mut session, Vec::new(), &glyphs));
298 assert_eq!(first, again);
299 }
300
301 #[test]
304 fn two_answers_that_differ_are_not_equal() {
305 let resting = Chrome::of(&viewed(Vec::new()));
306 let armed = Chrome::of(&viewed(vec![Event::Command(CommandId::Arm(
307 ToolName::NewBlock,
308 ))]));
309 assert_ne!(resting.tool, armed.tool);
310 assert_ne!(resting, armed);
311 }
312
313 #[test]
314 fn a_bound_chord_names_the_command_the_table_binds_it_to() {
315 let commands = Chrome::of(&viewed(Vec::new())).commands;
316 assert_eq!(
317 commands.bound(chord(Key::B)),
318 Some(CommandId::Arm(ToolName::NewBlock)),
319 );
320 assert_eq!(
321 commands.bound(Chord {
322 modifiers: Modifiers::None,
323 key: Key::Num4,
324 }),
325 Some(CommandId::Arm(ToolName::Route)),
326 );
327 assert_eq!(
328 commands.bound(Chord {
329 modifiers: Modifiers::Command,
330 key: Key::Num0,
331 }),
332 Some(CommandId::FitView),
333 );
334 }
335
336 #[test]
339 fn a_chord_this_call_does_not_offer_raises_nothing() {
340 let commands = Chrome::of(&viewed(Vec::new())).commands;
341 assert_eq!(
342 commands.face(CommandId::Undo),
343 None,
344 "a fresh session has nothing to undo",
345 );
346 assert!(commands.bound(chord(Key::E)).is_some());
347 let undone = Chrome::of(&viewed(vec![Event::Move(blockworx_paint::Move::Pan(
348 blockworx_geom::Vec2::new(40.0, 0.0),
349 ))]));
350 assert_eq!(
351 undone
352 .commands
353 .face(CommandId::Undo)
354 .map(|face| face.availability),
355 Some(Availability::Live),
356 "a camera move is a step the session can take back",
357 );
358 }
359}