1use core::time::Duration;
19use std::cell::{Cell, RefCell};
20use std::rc::Rc;
21
22use blockworx_canvas2d::input::{Focus, Reader, Sample};
23use blockworx_canvas2d::{
24 DevicePixelRatio, Glyphs, Images, Repaint, clipboard_write, css_color, css_cursor, download,
25 fit, paint_ground, replay,
26};
27use blockworx_doc::id::{BlockId, PinId};
28use blockworx_editor::shape::RoleTarget;
29use blockworx_editor::title_block::TitleBlock;
30use blockworx_export::bytes_for;
31use blockworx_geom::{Pos2, Rect, Vec2, pos2, vec2};
32use blockworx_kernel::Sheet;
33use blockworx_kernel::{Event, FrameRate, Handoff, Session, View, kernel};
34use blockworx_paint::theme::Theme;
35use blockworx_paint::{DrawList, FontChoice, Ground, Palette, Scheme, Tick, Vantage};
36use blockworx_store::doc::Doc;
37use blockworx_store::handle::Store;
38use blockworx_store::history;
39use blockworx_store::storage::{Any, DocumentRef, Name};
40use blockworx_tools::commands::{Act, Effect};
41use blockworx_tools::tool::Action;
42use dioxus::prelude::*;
43use wasm_bindgen::{JsCast as _, JsValue, prelude::Closure};
44use web_sys::{CanvasRenderingContext2d, Document, HtmlCanvasElement, HtmlElement, Window};
45
46use crate::chords::Token;
47use crate::chrome::Chrome;
48use crate::library::{Library, Listed, Opened};
49use crate::meter::{Frame, FrameMeter, Metered};
50use crate::mode::Prefers;
51use crate::pacing::{Pacing, Schedule};
52use crate::prefs::Preferences;
53use crate::sidebar::Section;
54
55struct Editor {
58 session: Session,
59 glyphs: Glyphs,
63 images: Images,
64 reader: Reader,
65 surface: Option<Surface>,
68 diagram: Option<Diagram>,
70 bands: Vec<(Band, Rect)>,
74 dressed: Dressed,
77 refused: Vec<String>,
80 raised: Vec<Effect>,
84 meter: FrameMeter,
86 metered: Option<Metered>,
88}
89
90#[derive(Clone, Copy, PartialEq, Eq, Debug)]
93pub enum Band {
94 Toolbar,
95 Status,
96 Notices,
97 Sheet,
100}
101
102#[derive(Clone, PartialEq)]
105struct Dressed {
106 palette: Palette,
107 font: FontChoice,
108 scheme: Scheme,
109}
110
111impl Dressed {
112 fn theme(&self) -> Theme {
116 let mut theme = Theme::default();
117 theme.set_palette(self.palette.clone());
118 theme
119 }
120}
121
122#[derive(Clone, PartialEq, Debug)]
125pub enum Picker {
126 Accent(RoleTarget),
127 PinType(Vec<PinId>),
128}
129
130#[derive(Clone, Copy, PartialEq, Eq, Debug)]
133pub enum Escaped {
134 Claimed,
135 Free,
136}
137
138#[derive(Clone, Copy, PartialEq, Eq, Debug)]
142pub enum Wanted {
143 Icon(BlockId),
144 Image,
145 Import,
146}
147
148#[derive(Clone, Copy, PartialEq, Eq, Debug)]
158pub enum Pasting {
159 Asked,
161 Provoked,
164}
165
166#[derive(Clone, PartialEq, Eq, Debug, Default)]
174pub struct Reported {
175 pub said: u64,
176 pub what: Option<String>,
177}
178
179#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
183pub struct Owed(pub usize);
184
185enum Door {
191 Drained,
193 Born,
194 Named(DocumentRef),
195 Renamed(String),
196 Deleted,
197 Exported,
198 DeletedNamed(Name),
200 ExportedNamed(Name),
201 Imported {
202 called: String,
203 archive: Vec<u8>,
204 },
205}
206
207enum Stood {
209 Carried(Doc),
212 Opened(Doc),
215}
216
217fn opened(got: Result<Store<Any>, String>, lent: Doc) -> (Stood, Option<String>) {
219 match got {
220 Ok(store) => (Stood::Opened(Doc::attached(store)), None),
221 Err(why) => (Stood::Carried(lent), Some(why)),
222 }
223}
224
225const NO_CONTAINER: &str = "This session has no diagram in the origin's storage";
227
228struct Surface {
230 canvas: HtmlCanvasElement,
231 ctx: CanvasRenderingContext2d,
232}
233
234struct Diagram {
237 draw_list: DrawList,
238 ground: Ground,
239 vantage: Vantage,
240}
241
242impl Diagram {
243 fn of(view: &mut View) -> Self {
244 Self {
245 draw_list: std::mem::take(&mut view.draw_list),
246 ground: Ground {
247 background: view.ground.background,
248 grid: view.ground.grid,
249 },
250 vantage: view.vantage,
251 }
252 }
253}
254
255#[derive(Default)]
257struct Batch {
258 events: Vec<Event>,
259 samples: Vec<Sample>,
263}
264
265#[derive(Clone)]
267pub struct Shell {
268 editor: Rc<RefCell<Editor>>,
269 library: Rc<Library>,
271 away: Rc<Cell<bool>>,
276 pasting: Rc<Cell<Pasting>>,
278 waiting: Rc<RefCell<Option<Door>>>,
281 batch: Rc<RefCell<Batch>>,
282 pacing: Rc<Cell<Pacing>>,
283 chrome: Signal<Chrome>,
284 frames: Signal<Option<Metered>>,
286 reported: Signal<Reported>,
288 owed: Signal<Owed>,
290 held: Signal<Vec<Listed>>,
292 renaming: Signal<u64>,
296 picker: Signal<Option<Picker>>,
298 wanted: Signal<Option<Wanted>>,
300 safe: Signal<Rect>,
303 palette: Signal<bool>,
306 section: Signal<Option<Section>>,
309 token: Token,
312}
313
314impl PartialEq for Shell {
317 fn eq(&self, other: &Self) -> bool {
318 Rc::ptr_eq(&self.editor, &other.editor)
319 }
320}
321
322impl Shell {
323 #[must_use]
327 pub fn opening(
328 prefs: &Preferences,
329 prefers: Prefers,
330 opened: Opened,
331 ) -> (Self, Signal<Chrome>) {
332 let Opened {
333 library,
334 doc,
335 notice,
336 } = opened;
337 let dressed = Dressed {
338 palette: prefs.palette(prefers),
339 font: prefs.font,
340 scheme: prefs.scheme,
341 };
342 let mut editor = Editor {
343 session: Session::opening(doc, prefs.identity()),
344 glyphs: Glyphs::new(dressed.font),
345 images: Images::default(),
346 reader: Reader::default(),
347 surface: None,
348 diagram: None,
349 bands: Vec::new(),
350 dressed: dressed.clone(),
351 refused: Vec::new(),
352 raised: Vec::new(),
353 meter: FrameMeter::default(),
354 metered: None,
355 };
356 if let Some(notice) = notice {
357 editor.session.failures.report(notice);
358 }
359 if let Some(named) = editor.session.doc.container_name() {
360 library.remembers(&named);
361 }
362 let primed = editor.run(vec![Event::Action(Action::SetPalette(dressed.palette))]);
363 let chrome = Signal::new(Chrome::of(&primed));
364 let shell = Self {
365 editor: Rc::new(RefCell::new(editor)),
366 library: Rc::new(library),
367 away: Rc::new(Cell::new(false)),
368 pasting: Rc::new(Cell::new(Pasting::Asked)),
369 waiting: Rc::new(RefCell::new(None)),
370 batch: Rc::new(RefCell::new(Batch::default())),
371 pacing: Rc::new(Cell::new(Pacing::default())),
372 chrome,
373 frames: Signal::new(None),
374 reported: Signal::new(Reported::default()),
375 owed: Signal::new(Owed::default()),
376 held: Signal::new(Vec::new()),
377 renaming: Signal::new(0),
378 picker: Signal::new(None),
379 wanted: Signal::new(None),
380 safe: Signal::new(Rect::ZERO),
381 palette: Signal::new(false),
382 section: Signal::new(None),
383 token: window().map_or_else(Token::default, |window| Token::of(&window)),
384 };
385 {
386 let listing = shell.clone();
387 spawn(async move { listing.lists_the_origin().await });
388 }
389 (shell, chrome)
390 }
391
392 #[cfg(test)]
396 #[must_use]
397 pub fn showing(chrome: Chrome) -> Self {
398 let (shell, mut signal) =
399 Self::opening(&Preferences::default(), Prefers::Light, Opened::detached());
400 signal.set(chrome);
401 let mut safe = shell.safe;
405 safe.set(Rect::from_min_max(pos2(0.0, 54.0), pos2(1200.0, 800.0)));
406 shell
407 }
408
409 #[must_use]
411 pub fn chrome(&self) -> Signal<Chrome> {
412 self.chrome
413 }
414
415 #[must_use]
417 pub fn frames(&self) -> Signal<Option<Metered>> {
418 self.frames
419 }
420
421 #[must_use]
423 pub fn reported(&self) -> Signal<Reported> {
424 self.reported
425 }
426
427 #[must_use]
429 pub fn owed(&self) -> Signal<Owed> {
430 self.owed
431 }
432
433 pub fn owes(&self, depth: Owed) {
435 let mut owed = self.owed;
436 if *owed.peek() != depth {
437 owed.set(depth);
438 }
439 }
440
441 #[must_use]
443 pub fn picker(&self) -> Signal<Option<Picker>> {
444 self.picker
445 }
446
447 #[must_use]
449 pub fn wanted(&self) -> Signal<Option<Wanted>> {
450 self.wanted
451 }
452
453 #[must_use]
455 pub fn safe(&self) -> Signal<Rect> {
456 self.safe
457 }
458
459 #[must_use]
461 pub fn palette(&self) -> Signal<bool> {
462 self.palette
463 }
464
465 #[must_use]
467 pub fn section(&self) -> Signal<Option<Section>> {
468 self.section
469 }
470
471 pub fn works(&self) {
475 let mut section = self.section;
476 if section.peek().is_some() {
477 section.set(None);
478 }
479 }
480
481 #[must_use]
485 pub fn escaped(&self) -> Escaped {
486 let mut palette = self.palette;
487 let mut section = self.section;
488 if *palette.peek() {
489 palette.set(false);
490 return Escaped::Claimed;
491 }
492 if section.peek().is_some() {
493 section.set(None);
494 return Escaped::Claimed;
495 }
496 Escaped::Free
497 }
498
499 #[must_use]
502 pub fn theme(&self) -> Theme {
503 self.editor.borrow().dressed.theme()
504 }
505
506 #[must_use]
508 pub fn token(&self) -> Token {
509 self.token
510 }
511
512 #[must_use]
514 pub fn attributed(&self) -> String {
515 self.editor.borrow().session.identity.name.clone()
516 }
517
518 pub fn wears(&self, prefs: &Preferences, prefers: Prefers) {
522 let asked = Dressed {
523 palette: prefs.palette(prefers),
524 font: prefs.font,
525 scheme: prefs.scheme,
526 };
527 let was = {
528 let mut editor = self.editor.borrow_mut();
529 editor.session.identity = prefs.identity();
530 let was = std::mem::replace(&mut editor.dressed, asked.clone());
531 if was.font != asked.font {
532 editor.glyphs = Glyphs::new(asked.font);
533 }
534 was
535 };
536 if was.palette != asked.palette {
537 self.say(Event::Action(Action::SetPalette(asked.palette)));
538 } else if was.font != asked.font {
539 self.books(Pacing::said);
542 }
543 }
544
545 pub fn failed(&self, what: impl Into<String>) {
549 let what = what.into();
550 tracing::error!("{what}");
551 let mut reported = self.reported;
552 let said = reported.peek().said + 1;
553 reported.set(Reported {
554 said,
555 what: Some(what),
556 });
557 }
558
559 pub fn raises(&self, act: Act) {
562 match act {
563 Act::Edit(action) => self.say(Event::Action(action)),
564 Act::Effect(effect) => self.performs(&effect),
565 }
566 }
567
568 fn performs(&self, effect: &Effect) {
571 let mut picker = self.picker;
572 let mut wanted = self.wanted;
573 match effect {
574 Effect::NewDocument => self.opens_a_door(Door::Born),
575 Effect::OpenRecent(named) => self.opens_a_door(Door::Named(named.clone())),
576 Effect::RenameDocument(to) => self.opens_a_door(Door::Renamed(to.clone())),
577 Effect::Accent(target) => picker.set(Some(Picker::Accent(*target))),
578 Effect::PinType(pins) => picker.set(Some(Picker::PinType(pins.clone()))),
579 Effect::AddIcon(block) => wanted.set(Some(Wanted::Icon(*block))),
580 Effect::AddImage => wanted.set(Some(Wanted::Image)),
581 Effect::Import => wanted.set(Some(Wanted::Import)),
582 Effect::Search => {
583 let mut palette = self.palette;
584 let open = !*palette.peek();
585 palette.set(open);
586 }
587 Effect::PickFile(_) => {
590 self.failed("A browser cannot open a diagram from disk; use Import .bwx.zip");
591 }
592 }
593 }
594
595 #[must_use]
599 pub fn held(&self) -> Signal<Vec<Listed>> {
600 self.held
601 }
602
603 pub fn asks_to_rename(&self) {
606 let mut renaming = self.renaming;
607 let asked = *renaming.peek() + 1;
608 renaming.set(asked);
609 }
610
611 #[must_use]
614 pub fn renaming(&self) -> Signal<u64> {
615 self.renaming
616 }
617
618 pub fn deletes(&self) {
621 self.opens_a_door(Door::Deleted);
622 }
623
624 pub fn exports_archive(&self) {
627 self.opens_a_door(Door::Exported);
628 }
629
630 pub fn exports_named(&self, name: &str) {
633 match Name::new(name) {
634 Some(named) => self.opens_a_door(Door::ExportedNamed(named)),
635 None => self.failed(format!("{name} is not a diagram")),
636 }
637 }
638
639 pub fn deletes_named(&self, name: &str) {
643 match Name::new(name) {
644 Some(named) => self.opens_a_door(Door::DeletedNamed(named)),
645 None => self.failed(format!("{name} is not a diagram")),
646 }
647 }
648
649 pub fn imports_archive(&self, called: String, archive: Vec<u8>) {
652 self.opens_a_door(Door::Imported { called, archive });
653 }
654
655 fn opens_a_door(&self, door: Door) {
663 if self.away.get() {
664 if !matches!(door, Door::Drained) {
667 self.waiting.replace(Some(door));
668 }
669 return;
670 }
671 self.away.set(true);
672 let lists = !matches!(door, Door::Drained);
675 let lent = {
676 let mut editor = self.editor.borrow_mut();
677 std::mem::take(&mut editor.session.doc)
678 };
679 let shell = self.clone();
680 spawn(async move {
681 let (stood, failure) = shell.walks_through(door, lent).await;
682 shell.takes_back(stood);
683 if let Some(failure) = failure {
684 shell.reports(failure);
685 }
686 if lists {
687 shell.lists_the_origin().await;
688 }
689 });
690 }
691
692 async fn walks_through(&self, door: Door, mut doc: Doc) -> (Stood, Option<String>) {
694 match door {
695 Door::Drained => {
700 let started = window().as_ref().and_then(clock_of);
701 let drained = doc.drain().await;
702 if let Ok(drained) = &drained {
703 tracing::info!(took = ?elapsed(started), ?drained, "wrote the journal to the origin");
704 }
705 let failure = drained.err().map(|why| {
706 format!("The diagram could not be written, and is now read-only: {why}")
707 });
708 (Stood::Carried(doc), failure)
709 }
710 Door::Born => opened(self.library.born().await, doc),
711 Door::Named(named) => opened(self.library.opens(&named).await, doc),
712 Door::Renamed(to) => {
713 let failure = self.library.renames(&mut doc, &to).await.err();
714 (Stood::Carried(doc), failure)
715 }
716 Door::Deleted => self.deletes_this(doc).await,
717 Door::Exported => self.exports_this(doc).await,
718 Door::DeletedNamed(named) if doc.container_name().as_ref() == Some(&named) => {
721 self.deletes_this(doc).await
722 }
723 Door::ExportedNamed(named) if doc.container_name().as_ref() == Some(&named) => {
724 self.exports_this(doc).await
725 }
726 Door::DeletedNamed(named) => {
727 let failure = self.library.removes_closed(&named).await.err();
728 (Stood::Carried(doc), failure)
729 }
730 Door::ExportedNamed(named) => {
731 let failure = self.delivers_archive(&named).await.err();
732 (Stood::Carried(doc), failure)
733 }
734 Door::Imported { called, archive } => {
735 opened(self.library.unpacks(&called, &archive).await, doc)
736 }
737 }
738 }
739
740 async fn deletes_this(&self, doc: Doc) -> (Stood, Option<String>) {
743 let Some(named) = doc.container_name() else {
744 return (Stood::Carried(doc), Some(NO_CONTAINER.to_owned()));
745 };
746 drop(doc);
751 if let Err(why) = self.library.removes(&named).await {
752 let back = self.library.opens(&DocumentRef::new(named.as_str())).await;
753 return match back {
754 Ok(store) => (Stood::Opened(Doc::attached(store)), Some(why)),
755 Err(also) => (Stood::Opened(Doc::default()), Some(format!("{why} {also}"))),
756 };
757 }
758 let (opened, failure) = self.library.stands_on().await;
759 (Stood::Opened(opened), failure)
760 }
761
762 async fn exports_this(&self, mut doc: Doc) -> (Stood, Option<String>) {
764 let Some(named) = doc.container_name() else {
765 return (Stood::Carried(doc), Some(NO_CONTAINER.to_owned()));
766 };
767 let failure = match doc.drain().await {
770 Ok(_) => self.delivers_archive(&named).await.err(),
771 Err(why) => Some(format!("Failed to export {named}: {why}")),
772 };
773 (Stood::Carried(doc), failure)
774 }
775
776 pub fn picked(&self, act: Option<Act>) {
780 let mut picker = self.picker;
781 picker.set(None);
782 if let Some(act) = act {
783 self.raises(act);
784 }
785 }
786
787 pub fn provoked(&self) {
790 self.pasting.set(Pasting::Provoked);
791 }
792
793 pub fn asked(&self) {
796 self.pasting.set(Pasting::Asked);
797 }
798
799 pub fn takes_a_paste(&self) -> bool {
802 self.pasting.replace(Pasting::Asked) == Pasting::Asked
803 }
804
805 pub fn delivers(&self, what: Wanted, name: &str, bytes: Vec<u8>) {
809 let mut wanted = self.wanted;
810 wanted.set(None);
811 match what {
812 Wanted::Import => {
813 self.editor
814 .borrow_mut()
815 .session
816 .handle_imported(name, bytes);
817 self.books(Pacing::said);
818 }
819 other => {
820 let Some(asset) = blockworx_editor::import::interpret(name, bytes) else {
821 self.failed(format!("{name} is not a PNG or an SVG"));
822 return;
823 };
824 self.say(Event::Action(match other {
825 Wanted::Icon(block) => Action::SetIcon { block, asset },
826 _ => Action::PlaceImage { asset },
827 }));
828 }
829 }
830 }
831
832 pub fn embeds(&self, name: &str, bytes: &[u8]) {
838 self.editor
839 .borrow_mut()
840 .session
841 .handle_embedded(name, bytes);
842 self.books(Pacing::said);
843 }
844
845 async fn delivers_archive(&self, named: &blockworx_store::storage::Name) -> Result<(), String> {
847 let archive = self.library.packs(named).await?;
848 let file = format!("{named}.zip");
849 download(&file, "application/zip", &archive)
850 .map_err(|refused| format!("Could not save {file}: {refused:?}"))
851 }
852
853 fn takes_back(&self, stood: Stood) {
855 {
856 let mut editor = self.editor.borrow_mut();
857 match stood {
858 Stood::Carried(doc) => editor.session.doc = doc,
859 Stood::Opened(doc) => {
860 editor.session.opens(doc);
861 }
862 }
863 if let Some(named) = editor.session.doc.container_name() {
864 self.library.remembers(&named);
865 }
866 self.owes(Owed(editor.session.doc.pending()));
867 }
868 self.away.set(false);
869 self.books(Pacing::said);
870 let Some(door) = self.waiting.borrow_mut().take() else {
871 return;
872 };
873 if self.editor.borrow().session.doc.pending() > 0 {
876 self.waiting.replace(Some(door));
877 self.opens_a_door(Door::Drained);
878 } else {
879 self.opens_a_door(door);
880 }
881 }
882
883 async fn lists_the_origin(&self) {
884 let held = self.library.listing().await;
885 let mut signal = self.held;
886 if *signal.peek() != held {
887 signal.set(held);
888 }
889 }
890
891 fn drains(&self) {
893 let owed = self.editor.borrow().session.doc.pending();
894 self.owes(Owed(owed));
895 if owed > 0 {
896 self.opens_a_door(Door::Drained);
897 }
898 }
899
900 pub fn reports(&self, what: impl Into<String>) {
903 let what = what.into();
904 tracing::error!("{what}");
905 self.editor.borrow_mut().session.failures.report(what);
906 self.books(Pacing::said);
907 }
908
909 pub fn unpicked(&self) {
912 let mut wanted = self.wanted;
913 wanted.set(None);
914 }
915
916 #[must_use]
919 pub fn typeface(&self) -> FontChoice {
920 use blockworx_paint::TextLayout as _;
921 self.editor.borrow().glyphs.typeface()
922 }
923
924 pub fn say(&self, event: Event) {
926 self.batch.borrow_mut().events.push(event);
927 self.books(Pacing::said);
928 }
929
930 pub fn sampled(&self, sample: Sample) {
932 self.batch.borrow_mut().samples.push(sample);
933 self.books(Pacing::said);
934 }
935
936 pub fn mounted(&self, canvas: HtmlCanvasElement) {
939 let Some(ctx) = context_of(&canvas) else {
940 tracing::error!("the canvas has no 2d context");
941 return;
942 };
943 let _ = canvas.focus();
944 self.editor.borrow_mut().surface = Some(Surface { canvas, ctx });
945 if let Some(loading) = window()
948 .and_then(|window| window.document())
949 .and_then(|document| document.get_element_by_id("bw-loading"))
950 {
951 loading.remove();
952 }
953 self.resized();
954 }
955
956 pub fn resized(&self) {
959 let Some(window) = window() else {
960 return;
961 };
962 let viewport = {
963 let editor = self.editor.borrow();
964 let Some(surface) = &editor.surface else {
965 return;
966 };
967 let viewport = fit(
968 &surface.canvas,
969 &surface.ctx,
970 css_size(&surface.canvas),
971 DevicePixelRatio::of(&window),
972 );
973 editor.repaints(viewport);
974 viewport
975 };
976 self.say(Event::Viewport(viewport));
977 self.says_the_safe_region(viewport);
978 }
979
980 pub fn covers(&self, band: Band, at: Rect) {
985 {
986 let mut editor = self.editor.borrow_mut();
987 match editor.bands.iter_mut().find(|(which, _)| *which == band) {
988 Some(stood) if stood.1 == at => return,
989 Some(stood) => stood.1 = at,
990 None => editor.bands.push((band, at)),
991 }
992 }
993 let viewport = self.editor.borrow().session.viewport();
994 self.says_the_safe_region(viewport);
995 }
996
997 #[must_use]
1000 pub fn origin(&self) -> Option<Pos2> {
1001 let editor = self.editor.borrow();
1002 let bounds = editor.surface.as_ref()?.canvas.get_bounding_client_rect();
1003 Some(pos2(bounds.left() as f32, bounds.top() as f32))
1004 }
1005
1006 pub fn grabs(&self, pointer: i32) {
1009 let editor = self.editor.borrow();
1010 let Some(surface) = &editor.surface else {
1011 return;
1012 };
1013 let _ = surface.canvas.set_pointer_capture(pointer);
1014 let _ = surface.canvas.focus();
1015 }
1016
1017 pub fn takes_keyboard(&self) {
1020 let editor = self.editor.borrow();
1021 if let Some(surface) = &editor.surface {
1022 let _ = surface.canvas.focus();
1023 }
1024 }
1025
1026 fn says_the_safe_region(&self, viewport: Rect) {
1027 let over: Vec<Rect> = self
1028 .editor
1029 .borrow()
1030 .bands
1031 .iter()
1032 .map(|(_, at)| *at)
1033 .collect();
1034 let clear = clear_of(viewport, &over);
1035 let mut safe = self.safe;
1036 if *safe.peek() != clear {
1037 safe.set(clear);
1038 }
1039 self.say(Event::Safe(clear));
1040 }
1041
1042 fn books(&self, ask: impl FnOnce(&mut Pacing) -> Schedule) {
1043 let mut pacing = self.pacing.get();
1044 let schedule = ask(&mut pacing);
1045 self.pacing.set(pacing);
1046 self.book(schedule);
1047 }
1048
1049 fn book(&self, schedule: Schedule) {
1050 let Some(window) = window() else {
1051 return;
1052 };
1053 match schedule {
1054 Schedule::Idle => {}
1055 Schedule::Frame => {
1056 let shell = self.clone();
1057 let _ = request_animation_frame(&window, move || shell.frame());
1058 }
1059 Schedule::Timer { ticket, after } => {
1060 let shell = self.clone();
1061 let _ = set_timeout(&window, after, move || {
1062 shell.books(|pacing| pacing.woke(ticket));
1063 });
1064 }
1065 }
1066 }
1067
1068 fn frame(&self) {
1075 self.books(Pacing::entered);
1076 if self.away.get() {
1077 return;
1078 }
1079 let batch = self.assemble();
1080 let (view, refused, raised, metered) = {
1081 let mut editor = self.editor.borrow_mut();
1082 let view = editor.run(batch);
1083 (
1084 view,
1085 std::mem::take(&mut editor.refused),
1086 std::mem::take(&mut editor.raised),
1087 editor.metered,
1088 )
1089 };
1090 if view.frame_rate == FrameRate::Shown {
1091 let mut frames = self.frames;
1092 frames.set(metered);
1093 }
1094 for refusal in refused {
1095 self.failed(refusal);
1096 }
1097 for effect in raised {
1100 self.performs(&effect);
1101 }
1102 let shown = Chrome::of(&view);
1103 if *self.chrome.peek() != shown {
1104 let mut chrome = self.chrome;
1105 chrome.set(shown);
1106 }
1107 self.books(|pacing| pacing.ran(view.repaint));
1108 self.drains();
1109 }
1110
1111 fn assemble(&self) -> Vec<Event> {
1114 let Batch { events, samples } = std::mem::take(&mut *self.batch.borrow_mut());
1115 let focus = focus();
1116 let mut batch = Vec::with_capacity(events.len() + samples.len() + 2);
1117 if let Some(tick) = window().and_then(|window| tick(&window)) {
1118 batch.push(Event::Tick(tick));
1119 }
1120 batch.extend(events);
1121 let read = self.editor.borrow_mut().reader.read(&samples, focus);
1122 batch.extend(read.moves.into_iter().map(Event::Move));
1123 batch.extend(read.input.raw.into_iter().map(Event::Pointer));
1124 batch.push(Event::Keys(read.input.keys));
1125 batch
1126 }
1127}
1128
1129impl Editor {
1130 fn states_the_sheet(&mut self) {
1135 let name = self.session.document_name(None);
1139 let rev = self.session.viewed_repo().rev();
1140 self.session.sheet = Sheet {
1141 block: TitleBlock {
1142 name: name.clone(),
1143 author: self.session.identity.name.clone(),
1144 rev,
1145 date: self.session.written_at(rev).map(history::date),
1146 from: None,
1147 },
1148 name,
1149 scheme: self.dressed.scheme,
1150 };
1151 }
1152
1153 fn run(&mut self, batch: Vec<Event>) -> View {
1155 self.states_the_sheet();
1156 let clock = || window().as_ref().and_then(clock_of);
1157 let started = clock();
1158 let mut view = kernel(&mut self.session, batch, &self.glyphs);
1159 let answered = clock();
1160 for handoff in std::mem::take(&mut view.handoffs) {
1161 self.deliver(handoff);
1162 }
1163 self.raised.append(&mut view.effects);
1164 let painting = clock();
1165 let owed = self.shows(Diagram::of(&mut view), view.viewport);
1166 if let (Some(started), Some(answered), Some(painting), Some(painted)) =
1167 (started, answered, painting, clock())
1168 {
1169 self.metered = Some(self.meter.record(Frame {
1170 at: started,
1171 kernel: answered.saturating_sub(started),
1172 paint: painted.saturating_sub(painting),
1173 }));
1174 }
1175 self.wears(view.cursor);
1176 if owed == Repaint::Owed {
1180 view.repaint = Some(view.repaint.unwrap_or(Duration::ZERO));
1181 }
1182 view
1183 }
1184
1185 fn deliver(&mut self, handoff: Handoff) {
1186 match handoff {
1187 Handoff::Asset { hash, asset } => self.images.register(hash, &asset),
1188 Handoff::Clipboard(text) => {
1189 if let Err(refused) = clipboard_write(&text) {
1190 self.refused
1191 .push(format!("The clipboard refused the copy: {refused:?}"));
1192 }
1193 }
1194 Handoff::Export { content, name } => {
1195 let named = format!("{name}.{}", content.extension());
1196 if let Err(refused) = download(&named, content.mime(), &bytes_for(&content)) {
1197 self.refused
1198 .push(format!("Could not save {named}: {refused:?}"));
1199 }
1200 }
1201 }
1202 }
1203
1204 fn shows(&mut self, diagram: Diagram, viewport: Rect) -> Repaint {
1206 let owed = self.paint(&diagram, viewport);
1207 let reground = self
1208 .diagram
1209 .as_ref()
1210 .is_none_or(|was| was.ground.background != diagram.ground.background);
1211 if reground {
1212 grounds_the_page(diagram.ground.background);
1213 }
1214 self.diagram = Some(diagram);
1215 owed
1216 }
1217
1218 fn repaints(&self, viewport: Rect) {
1222 if let Some(diagram) = &self.diagram {
1223 self.paint(diagram, viewport);
1224 }
1225 }
1226
1227 fn paint(&self, diagram: &Diagram, viewport: Rect) -> Repaint {
1228 let Some(surface) = &self.surface else {
1229 return Repaint::Settled;
1230 };
1231 paint_ground(&surface.ctx, viewport, diagram.vantage, diagram.ground);
1232 replay(&diagram.draw_list, &surface.ctx, &self.images, &self.glyphs)
1233 }
1234
1235 fn wears(&self, cursor: Option<blockworx_paint::Cursor>) {
1236 let Some(surface) = &self.surface else {
1237 return;
1238 };
1239 let shape = cursor.map_or("default", css_cursor);
1240 let _ = surface.canvas.style().set_property("cursor", shape);
1241 }
1242}
1243
1244#[must_use]
1249pub fn clear_of(viewport: Rect, bands: &[Rect]) -> Rect {
1250 bands.iter().fold(viewport, |clear, band| {
1251 let band = band.intersect(clear);
1252 if band.width() <= 0.0 || band.height() <= 0.0 {
1253 return clear;
1254 }
1255 let hugged = [
1256 (
1257 band.min.x <= clear.min.x,
1258 band.max.x - clear.min.x,
1259 Edge::Left,
1260 ),
1261 (
1262 band.max.x >= clear.max.x,
1263 clear.max.x - band.min.x,
1264 Edge::Right,
1265 ),
1266 (
1267 band.min.y <= clear.min.y,
1268 band.max.y - clear.min.y,
1269 Edge::Top,
1270 ),
1271 (
1272 band.max.y >= clear.max.y,
1273 clear.max.y - band.min.y,
1274 Edge::Bottom,
1275 ),
1276 ];
1277 let Some(&(_, depth, edge)) = hugged
1278 .iter()
1279 .filter(|(hugs, _, _)| *hugs)
1280 .min_by(|(_, a, _), (_, b, _)| a.total_cmp(b))
1281 else {
1282 return clear;
1283 };
1284 match edge {
1285 Edge::Left => Rect::from_min_max(pos2(clear.min.x + depth, clear.min.y), clear.max),
1286 Edge::Right => Rect::from_min_max(clear.min, pos2(clear.max.x - depth, clear.max.y)),
1287 Edge::Top => Rect::from_min_max(pos2(clear.min.x, clear.min.y + depth), clear.max),
1288 Edge::Bottom => Rect::from_min_max(clear.min, pos2(clear.max.x, clear.max.y - depth)),
1289 }
1290 })
1291}
1292
1293#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1295enum Edge {
1296 Left,
1297 Right,
1298 Top,
1299 Bottom,
1300}
1301
1302fn focus() -> Focus {
1305 window()
1306 .and_then(|window| window.document())
1307 .map_or(Focus::Elsewhere, |document| focus_of(&document))
1308}
1309
1310fn focus_of(document: &Document) -> Focus {
1313 match document.active_element() {
1314 Some(active) if types_into(&active) => Focus::Elsewhere,
1315 _ => Focus::Canvas,
1316 }
1317}
1318
1319#[must_use]
1321pub fn types_into(element: &web_sys::Element) -> bool {
1322 matches!(element.tag_name().as_str(), "INPUT" | "TEXTAREA" | "SELECT")
1323 || element
1324 .dyn_ref::<HtmlElement>()
1325 .is_some_and(HtmlElement::is_content_editable)
1326}
1327
1328fn grounds_the_page(ground: blockworx_paint::Color) {
1331 let Some(root) = window()
1332 .and_then(|window| window.document())
1333 .and_then(|document| document.document_element())
1334 .and_then(|root| root.dyn_into::<HtmlElement>().ok())
1335 else {
1336 return;
1337 };
1338 let _ = root.style().set_property("--bw-ground", &css_color(ground));
1339}
1340
1341fn tick(window: &Window) -> Option<Tick> {
1344 clock_of(window).map(Tick::at)
1345}
1346
1347fn clock_of(window: &Window) -> Option<Duration> {
1349 let now = window.performance()?.now();
1350 Some(Duration::from_secs_f64(now.max(0.0) / 1_000.0))
1351}
1352
1353fn elapsed(started: Option<Duration>) -> Option<Duration> {
1355 let now = window().as_ref().and_then(clock_of)?;
1356 Some(now.saturating_sub(started?))
1357}
1358
1359fn css_size(canvas: &HtmlCanvasElement) -> Vec2 {
1360 let bounds = canvas.get_bounding_client_rect();
1361 vec2(bounds.width() as f32, bounds.height() as f32)
1362}
1363
1364fn context_of(canvas: &HtmlCanvasElement) -> Option<CanvasRenderingContext2d> {
1365 canvas
1366 .get_context("2d")
1367 .ok()
1368 .flatten()
1369 .and_then(|ctx| ctx.dyn_into::<CanvasRenderingContext2d>().ok())
1370}
1371
1372fn request_animation_frame(window: &Window, run: impl FnOnce() + 'static) -> Result<(), JsValue> {
1373 let once = Closure::once_into_js(run);
1374 window.request_animation_frame(once.unchecked_ref())?;
1375 Ok(())
1376}
1377
1378pub fn spawn(work: impl core::future::Future<Output = ()> + 'static) {
1386 #[cfg(target_arch = "wasm32")]
1387 wasm_bindgen_futures::spawn_local(work);
1388 #[cfg(not(target_arch = "wasm32"))]
1389 drop(work);
1390}
1391
1392#[must_use]
1399pub fn window() -> Option<Window> {
1400 #[cfg(target_arch = "wasm32")]
1401 {
1402 web_sys::window()
1403 }
1404 #[cfg(not(target_arch = "wasm32"))]
1405 {
1406 None
1407 }
1408}
1409
1410pub fn after(delay: Duration, then: impl FnOnce() + 'static) {
1417 if let Some(window) = window() {
1418 let _ = set_timeout(&window, delay, then);
1419 }
1420}
1421
1422#[must_use]
1426pub fn measured(element: &web_sys::Element) -> Option<Rect> {
1427 let band = element.dyn_ref::<web_sys::HtmlElement>()?;
1428 Some(Rect::from_min_size(
1429 pos2(band.offset_left() as f32, band.offset_top() as f32),
1430 vec2(band.offset_width() as f32, band.offset_height() as f32),
1431 ))
1432}
1433
1434fn set_timeout(
1435 window: &Window,
1436 after: Duration,
1437 run: impl FnOnce() + 'static,
1438) -> Result<(), JsValue> {
1439 let once = Closure::once_into_js(run);
1440 window.set_timeout_with_callback_and_timeout_and_arguments_0(
1441 once.unchecked_ref(),
1442 after.as_millis().min(i32::MAX as u128) as i32,
1443 )?;
1444 Ok(())
1445}
1446
1447#[cfg(test)]
1448mod tests {
1449 use super::*;
1450
1451 const VIEWPORT: Rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(800.0, 600.0));
1452
1453 fn band(min: Pos2, size: Vec2) -> Rect {
1454 Rect::from_min_size(min, size)
1455 }
1456
1457 #[test]
1458 fn nothing_over_the_diagram_leaves_the_whole_viewport_clear() {
1459 assert_eq!(clear_of(VIEWPORT, &[]), VIEWPORT);
1460 }
1461
1462 #[test]
1463 fn a_band_along_an_edge_is_taken_off_that_edge() {
1464 let status = band(pos2(0.0, 560.0), vec2(240.0, 40.0));
1465 assert!(
1466 VIEWPORT.intersects(status),
1467 "a band off the diagram would prove nothing",
1468 );
1469 assert_eq!(
1470 clear_of(VIEWPORT, &[status]),
1471 Rect::from_min_max(VIEWPORT.min, pos2(800.0, 560.0)),
1472 );
1473 let strip = band(pos2(0.0, 0.0), vec2(800.0, 54.0));
1474 assert_eq!(
1475 clear_of(VIEWPORT, &[strip]),
1476 Rect::from_min_max(pos2(0.0, 54.0), VIEWPORT.max),
1477 );
1478 }
1479
1480 #[test]
1483 fn bands_on_two_edges_are_both_taken_off() {
1484 let strip = band(pos2(0.0, 0.0), vec2(800.0, 54.0));
1485 let status = band(pos2(0.0, 560.0), vec2(240.0, 40.0));
1486 let clear = Rect::from_min_max(pos2(0.0, 54.0), pos2(800.0, 560.0));
1487 assert_eq!(clear_of(VIEWPORT, &[strip, status]), clear);
1488 assert_eq!(clear_of(VIEWPORT, &[status, strip]), clear);
1489 }
1490
1491 #[test]
1494 fn a_band_in_the_middle_takes_nothing_off() {
1495 let popup = band(pos2(300.0, 250.0), vec2(200.0, 100.0));
1496 assert_eq!(clear_of(VIEWPORT, &[popup]), VIEWPORT);
1497 }
1498}