diff --git a/crates/snowbound/src/attachment.rs b/crates/snowbound/src/attachment.rs index e0cab49b677159c00c2c96f6ab653794dccd1088..3d0718260154f2fddc97db255a7e10fc9e165308 100644 --- a/crates/snowbound/src/attachment.rs +++ b/crates/snowbound/src/attachment.rs @@ -10,10 +10,14 @@ use std::{ impl State { /// Inserts a file at the caret or window point `at`, as a picture when requested. pub(crate) fn import_file(&mut self, path: &Path, at: Option<[f32; 2]>, picture: bool) { - if self.session.as_ref().is_some_and(crate::Session::read_only) { + if self + .session + .as_ref() + .is_some_and(|session| session.read_only(&self.pane.showing)) + { return; } - let loading = self.loading; + let loading = self.pane.loading; let reply = self.reply( move |state, (path, read): (PathBuf, std::io::Result>)| { let bytes = match read { @@ -27,7 +31,7 @@ impl State { return Ok(()); } }; - if state.loading != loading { + if state.pane.loading != loading { platform::alert( "File wasn't inserted", "Return to the page and insert the file again.", @@ -37,7 +41,7 @@ impl State { if state .session .as_ref() - .is_some_and(crate::Session::read_only) + .is_some_and(|session| session.read_only(&state.pane.showing)) { return Ok(()); } @@ -60,8 +64,11 @@ impl State { tags: Vec::new(), }; let response = match at { - Some(point) => state.view.drop_attachment(state.page_point(point), file)?, - None => state.view.insert_attachment(file)?, + Some(point) => state + .pane + .view + .drop_attachment(state.page_point(point), file)?, + None => state.pane.view.insert_attachment(file)?, }; state.respond(response); Ok(()) diff --git a/crates/snowbound/src/commands.rs b/crates/snowbound/src/commands.rs index 41ce7eec7ec2726fec62873988fe55f12491f7f7..3484d2f6ecdc54e4cf7403f3d0bc39a9ef2c3030 100644 --- a/crates/snowbound/src/commands.rs +++ b/crates/snowbound/src/commands.rs @@ -1070,7 +1070,7 @@ impl State { } pub(crate) fn format_state(&self) -> FormatState { - self.view.editor.format_state().unwrap_or_default() + self.pane.view.editor.format_state().unwrap_or_default() } /// Queues `choice` to run after this frame's input, as a key or a click on it does. @@ -1127,7 +1127,7 @@ impl State { pub(crate) fn status(&self, choice: &Choice, format: &FormatState) -> Status { let session = self.session.as_ref(); - let welcome = session.is_none() && !self.temporary && self.sectionless.is_none(); + let welcome = session.is_none() && !self.temporary && self.pane.sectionless.is_none(); let modal = self.options.is_some() || self.password.is_some() || self.themes.is_some() @@ -1138,13 +1138,14 @@ impl State { #[cfg(any(windows, target_os = "linux"))] let modal = modal || self.prompt.strong_count() != 0; let page = (session.is_some() || self.temporary) && !modal; - let writable = page && !session.is_some_and(|session| session.read_only()); + let writable = + page && !session.is_some_and(|session| session.read_only(&self.pane.showing)); // Picked drawings leave the text selection behind them unseen, so it takes no edits. - let typing = self.view.accepts_text() && self.view.ink_selection().is_empty(); + let typing = self.pane.view.accepts_text() && self.pane.view.ink_selection().is_empty(); let text = writable && typing; let tagged = writable && matches!( - self.view.object_focus(), + self.pane.view.object_focus(), Some( canvas::interaction::ObjectFocus::Image(_) | canvas::interaction::ObjectFocus::File(_) @@ -1152,12 +1153,12 @@ impl State { ); // Palette commands act after its search field returns focus to the editor. let field = self.ui.focused_field(Some(crate::palette::id())).is_some(); - let [anchor, focus] = self.view.editor.selection().positions; + let [anchor, focus] = self.pane.view.editor.selection().positions; let selected = page && !field && (typing && anchor != focus || matches!( - self.view.object_focus(), + self.pane.view.object_focus(), Some(canvas::interaction::ObjectFocus::Image(_)) )); let enabled = |enabled| Status { @@ -1178,7 +1179,7 @@ impl State { Choice::Pen(_) => Id::Pen, Choice::Symbol(_) => Id::Symbol, }; - let tool = self.view.tool(); + let tool = self.pane.view.tool(); match id { Id::Settings | Id::NewNotebook @@ -1267,8 +1268,8 @@ impl State { checked: Some(false), }, |session| Status { - enabled: !session.page_versions(session.space).is_empty(), - checked: Some(session.shown_history == Some(session.space)), + enabled: !session.page_versions(self.pane.showing.space).is_empty(), + checked: Some(self.pane.showing.shown_history == Some(self.pane.showing.space)), }, ), Id::FullPageView => Status { @@ -1312,7 +1313,7 @@ impl State { }, Id::SnapToGrid => Status { enabled: !modal, - checked: Some(self.view.snap_to_grid), + checked: Some(self.pane.view.snap_to_grid), }, Id::HideSpelling => Status { enabled: !modal && self.spelling.is_some(), @@ -1320,9 +1321,9 @@ impl State { }, Id::MarkdownShortcuts => Status { enabled: !modal, - checked: Some(self.view.editor.markdown.is_some()), + checked: Some(self.pane.view.editor.markdown.is_some()), }, - Id::Spelling => enabled(text && self.view.spelling.is_some()), + Id::Spelling => enabled(text && self.pane.view.spelling.is_some()), Id::FormatPainter => Status { enabled: text, checked: Some(self.painter.is_some()), @@ -1343,7 +1344,7 @@ impl State { Id::Transport(transport) => self.transport_status(transport), Id::InsertSpace => Status { enabled: writable, - checked: Some(self.view.inserting_space()), + checked: Some(self.pane.view.inserting_space()), }, Id::SelectType => Status { enabled: page, @@ -1427,7 +1428,7 @@ impl State { if matches!(formatting, Formatting::Font(_)) || self.toolbar != kept { self.save_settings(); } - let response = self.view.format(formatting)?; + let response = self.pane.view.format(formatting)?; self.respond(response); Ok(()) } @@ -1442,7 +1443,7 @@ impl State { self.session .as_ref() .map(|session| &session.library) - .or(self.sectionless.as_ref()) + .or(self.pane.sectionless.as_ref()) } /// Runs `choice` where it applies now. @@ -1463,22 +1464,22 @@ impl State { Choice::Color(color) => return format(self, Formatting::Color(color)), Choice::List(style) => return format(self, Formatting::List(style)), Choice::Table([rows, columns]) => { - let response = self.view.insert_table(rows, columns)?; + let response = self.pane.view.insert_table(rows, columns)?; self.respond(response); return Ok(()); } Choice::PageColor(color) => { - return self.paper_page(color, self.view.editor.rule_lines(), None); + return self.paper_page(color, self.pane.view.editor.rule_lines(), None); } Choice::Art(name) => { return self.with_art(name.and_then(canvas::template::find), |state, art| { - let editor = &state.view.editor; + let editor = &state.pane.view.editor; state.paper_page(editor.page_color(), editor.rule_lines(), Some(art)) }); } Choice::RuleLines(lines) => { let lines = lines.map(|index| canvas::template::RULE_LINES[index].1); - return self.paper_page(self.view.editor.page_color(), lines, None); + return self.paper_page(self.pane.view.editor.page_color(), lines, None); } Choice::Pen(place) => { self.toolbar.pen = place; @@ -1495,7 +1496,7 @@ impl State { recent.insert(0, symbol); recent.truncate(crate::symbol::GALLERY); self.save_settings(); - let response = self.view.insert_symbol(symbol)?; + let response = self.pane.view.insert_symbol(symbol)?; self.respond(response); return Ok(()); } @@ -1544,18 +1545,19 @@ impl State { title: String::new(), }, Id::NewSubpage => Work::NewPage { - under: self.session.as_ref().map(|session| session.space), + under: self.session.as_ref().map(|_| self.pane.showing.space), title: String::new(), }, Id::PageVersions => { - let session = self.session.as_ref().ok_or("No notebook is open")?; + self.session.as_ref().ok_or("No notebook is open")?; Work::History { - page: session.space, - show: session.shown_history != Some(session.space), + page: self.pane.showing.space, + show: self.pane.showing.shown_history != Some(self.pane.showing.space), } } Id::CopyPageLink => { - let space = self.session.as_ref().ok_or("No notebook is open")?.space; + self.session.as_ref().ok_or("No notebook is open")?; + let space = self.pane.showing.space; let link = self.page_link(space, None)?; self.clipboard.set_text(link)?; return Ok(()); @@ -1604,7 +1606,7 @@ impl State { } Id::Undo | Id::Redo => return self.step(id == Id::Redo), Id::Cut | Id::Copy => { - let response = self.view.copy(id == Id::Cut)?; + let response = self.pane.view.copy(id == Id::Cut)?; self.respond(response); return Ok(()); } @@ -1624,7 +1626,7 @@ impl State { Id::FormatPainter => { self.painter = match self.painter { Some(_) => None, - None => Some(self.view.editor.painted_format()?), + None => Some(self.pane.view.editor.painted_format()?), }; return Ok(()); } @@ -1632,7 +1634,7 @@ impl State { match field { Some(field) => self.ui.focus_all(field), None => { - let response = self.view.widen_selection()?; + let response = self.pane.view.widen_selection()?; self.respond(response); } } @@ -1668,11 +1670,11 @@ impl State { } Id::ZoomIn | Id::ZoomOut | Id::ActualSize => { let zoom = match id { - Id::ZoomIn => self.view.zoom() * 1.1, - Id::ZoomOut => self.view.zoom() / 1.1, + Id::ZoomIn => self.pane.view.zoom() * 1.1, + Id::ZoomOut => self.pane.view.zoom() / 1.1, _ => 1.0, }; - let response = self.view.set_zoom(zoom)?; + let response = self.pane.view.set_zoom(zoom)?; self.respond(response); return Ok(()); } @@ -1725,12 +1727,12 @@ impl State { return Ok(()); } Id::SnapToGrid => { - self.view.snap_to_grid = !self.view.snap_to_grid; + self.pane.view.snap_to_grid = !self.pane.view.snap_to_grid; self.save_settings(); return Ok(()); } Id::MarkdownShortcuts => { - self.view.editor.markdown = markdown(self.view.editor.markdown.is_none()); + self.pane.view.editor.markdown = markdown(self.pane.view.editor.markdown.is_none()); self.save_settings(); return Ok(()); } @@ -1743,7 +1745,7 @@ impl State { Id::RecordAudio | Id::RecordVideo => return self.record(id == Id::RecordVideo), Id::Transport(transport) => return self.run_transport(transport), Id::InsertSpace => { - let response = self.view.insert_space(); + let response = self.pane.view.insert_space(); self.respond(response); return Ok(()); } @@ -1756,7 +1758,7 @@ impl State { Id::Shape(kind) => Tool::Shape(kind, pens[0].shape()), _ => Tool::Select, }; - let response = self.view.set_tool(tool); + let response = self.pane.view.set_tool(tool); self.respond(response); return Ok(()); } @@ -1765,7 +1767,7 @@ impl State { return Ok(()); } Id::Equation => { - let response = self.view.insert_equation()?; + let response = self.pane.view.insert_equation()?; self.respond(response); return Ok(()); } @@ -1779,7 +1781,7 @@ impl State { Id::Time => time, _ => format!("{date} {time}"), }; - let response = self.view.insert_text(format!("{text} "))?; + let response = self.pane.view.insert_text(format!("{text} "))?; self.respond(response); return Ok(()); } @@ -1803,7 +1805,7 @@ impl State { // Clear Formatting at a caret makes the paragraph Normal, as OneNote 2010's // Ctrl+Shift+N does; over a selection it clears the characters' formatting. Id::ClearFormatting => { - let [anchor, focus] = self.view.editor.selection().positions; + let [anchor, focus] = self.pane.view.editor.selection().positions; if anchor == focus { return format(self, Formatting::Style(self.gallery_style("p"))); } @@ -2096,9 +2098,9 @@ mod tests { for effect in [ "self.respond(", "self.changed", - "self.view.format(", - "self.view.set_zoom(", - "self.view.insert", + "self.pane.view.format(", + "self.pane.view.set_zoom(", + "self.pane.view.insert", "self.open_link_dialog(", ] { assert!( diff --git a/crates/snowbound/src/conflict_render.rs b/crates/snowbound/src/conflict_render.rs index f323baea357f69f8341c46692a3b9ab83735c36e..a15abd31f2103b8b36d7e3c67d7f3692621afd5e 100644 --- a/crates/snowbound/src/conflict_render.rs +++ b/crates/snowbound/src/conflict_render.rs @@ -10,7 +10,7 @@ const SCALE: f32 = 2.0; /// One view laid out: the bar and the page on the left, the page list on the right; `menu` /// opens the conflict page's menu. -fn layout(ui: &mut Ui, session: &Session, menu: bool) -> Vec { +fn layout(ui: &mut Ui, session: &Session, showing: &Showing, menu: bool) -> Vec { let theme = ui.theme.clone(); let section = theme.section(section_color(None)); ui.begin(SIZE, SCALE, Instant::now()); @@ -36,7 +36,7 @@ fn layout(ui: &mut Ui, session: &Session, menu: bool) -> Vec { }, ); let mut commands: Vec = session - .bar() + .bar(showing) .and_then(|bar| conflict_bar(ui, bar, &["synthetic", "Songs"], [false, true])) .into_iter() .collect(); @@ -64,6 +64,7 @@ fn layout(ui: &mut Ui, session: &Session, menu: bool) -> Vec { &theme, §ion, session, + showing, [&HashSet::new(); 2], &HashMap::new(), (0.0, false), @@ -242,8 +243,7 @@ fn conflict_views_offer_the_versions_and_render_offscreen() { let library = Arc::new(crate::Library::section(&file, &directory.join("cache"))); let section = library.open(&library.location, || {}).unwrap(); let path = library.location.clone(); - let (mut session, _) = read_session(section, library, path, None).unwrap(); - let page = session.space; + let (session, page, _) = read_session(section, library, path, None).unwrap(); let version = session.versions(page)[0].space; let output = std::env::var_os("SNOWBOUND_CONFLICT_RENDER").map(PathBuf::from); let mut gpu = output.as_deref().map(gpu); @@ -262,15 +262,18 @@ fn conflict_views_offer_the_versions_and_render_offscreen() { Theme::dark(), ), ] { - session.space = space; - session.shown = shown; + let showing = Showing { + space, + shown, + ..Showing::default() + }; let mut ui = Ui::new(appearance, std::time::Duration::from_millis(500)); // Later frames lay out with earlier frames' measurements, and a menu fades in. for _ in 0..8 { - layout(&mut ui, &session, menu); + layout(&mut ui, &session, &showing, menu); std::thread::sleep(std::time::Duration::from_millis(30)); } - let bar = session.bar(); + let bar = session.bar(&showing); match (name, bar) { ("page" | "page-dark", Some(Bar::Page { shown: false, .. })) | ("versions", Some(Bar::Page { shown: true, .. })) @@ -326,8 +329,7 @@ fn page_versions_list_open_read_only_and_render_offscreen() { let library = Arc::new(crate::Library::section(&file, &directory.join("cache"))); let section = library.open(&library.location, || {}).unwrap(); let path = library.location.clone(); - let (mut session, _) = read_session(section, library, path, None).unwrap(); - let page = session.space; + let (session, page, _) = read_session(section, library, path, None).unwrap(); let versions: Vec<_> = session .page_versions(page) .iter() @@ -374,10 +376,14 @@ fn page_versions_list_open_read_only_and_render_offscreen() { Theme::dark(), ), ] { - session.shown_history = shown; - session.version = version; - assert_eq!(session.read_only(), version.is_some(), "{name}"); - match (session.bar(), version) { + let showing = Showing { + space: page, + version, + shown_history: shown, + ..Showing::default() + }; + assert_eq!(session.read_only(&showing), version.is_some(), "{name}"); + match (session.bar(&showing), version) { ( Some(Bar::History { version: open, @@ -393,7 +399,7 @@ fn page_versions_list_open_read_only_and_render_offscreen() { } let mut ui = Ui::new(appearance, std::time::Duration::from_millis(500)); for _ in 0..8 { - layout(&mut ui, &session, menu); + layout(&mut ui, &session, &showing, menu); std::thread::sleep(std::time::Duration::from_millis(30)); } for context in &versions { diff --git a/crates/snowbound/src/history.rs b/crates/snowbound/src/history.rs index c5d98bb0a21c79e29d634f7d373188d248b2d573..f448ef400564a281bc0198c8dafa1302d997abef 100644 --- a/crates/snowbound/src/history.rs +++ b/crates/snowbound/src/history.rs @@ -25,9 +25,9 @@ impl Session { /// Whether the open page takes no edits: a conflict page, a version, or a page of the /// recycle bin. - pub(crate) fn read_only(&self) -> bool { - self.version.is_some() - || self.conflict(self.space).is_some() + pub(crate) fn read_only(&self, showing: &Showing) -> bool { + showing.version.is_some() + || self.conflict(showing.space).is_some() || crate::recycle::binned(&self.tabs[self.tab].path) } @@ -54,13 +54,13 @@ impl Session { } } - pub(crate) fn refresh_history(&mut self) -> Result<(), Box> { + pub(crate) fn refresh_history(&mut self, showing: &mut Showing) -> Result<(), Box> { self.history = self.section.versions()?; - if self + if showing .shown_history .is_some_and(|page| self.page_versions(page).is_empty()) { - self.shown_history = None; + showing.shown_history = None; } Ok(()) } @@ -142,9 +142,9 @@ pub fn menu( impl State { /// Shows or hides a page's versions under it; hiding leaves an open version for its page. pub(crate) fn show_history(&mut self, page: ExGuid, show: bool) -> Result<(), Box> { - let session = self.session.as_mut().ok_or("No section is open")?; - session.shown_history = show.then_some(page); - if !show && session.version.is_some() { + self.session.as_ref().ok_or("No section is open")?; + self.pane.showing.shown_history = show.then_some(page); + if !show && self.pane.showing.version.is_some() { self.commands.push(Command::OpenPage(page)); } Ok(()) @@ -174,7 +174,7 @@ impl State { .restore_version_async(page, version, &self.author); self.section_edit(operation, move |state, _| { if let Some(session) = &mut state.session { - session.refresh_history()?; + session.refresh_history(&mut state.pane.showing)?; } state.edited(vec![page]); state.commands.push(Command::OpenPage(page)); @@ -193,7 +193,7 @@ impl State { .delete_versions_async(&[(page, vec![version])]); self.section_edit(operation, move |state, _| { if let Some(session) = &mut state.session { - session.refresh_history()?; + session.refresh_history(&mut state.pane.showing)?; } state.commands.push(Command::OpenPage(page)); Ok(()) @@ -274,9 +274,11 @@ impl State { }; self.section_edit(operation, move |state, ()| { if let Some(session) = &mut state.session { - session.refresh_history()?; - if session.version.is_some() { - state.commands.push(Command::OpenPage(session.space)); + session.refresh_history(&mut state.pane.showing)?; + if state.pane.showing.version.is_some() { + state + .commands + .push(Command::OpenPage(state.pane.showing.space)); } } Ok(()) diff --git a/crates/snowbound/src/link.rs b/crates/snowbound/src/link.rs index 7d0067e8eb349a110e6440ad2e38f5689552e376..6c4dc59e3eacfab81b2c309af3909fda5ec302dd 100644 --- a/crates/snowbound/src/link.rs +++ b/crates/snowbound/src/link.rs @@ -75,7 +75,7 @@ impl State { /// Ctrl+K and the toolbar's Link: the dialog opens on what the selection links. pub(crate) fn open_link_dialog(&mut self) { // As OneNote 2010's, Link does nothing over a selection across paragraphs. - let Some((text, address)) = self.view.link_prefill() else { + let Some((text, address)) = self.pane.view.link_prefill() else { return; }; self.link = Some(LinkDialog { @@ -239,7 +239,7 @@ impl State { } let dialog = self.link.as_ref().unwrap(); if ok && !dialog.address.trim().is_empty() { - let response = self.view.set_link(&dialog.text, &dialog.address)?; + let response = self.pane.view.set_link(&dialog.text, &dialog.address)?; self.respond(response); } else if !cancel { return Ok(()); @@ -252,7 +252,7 @@ impl State { /// A secondary press on text, a picture or a file opens its context menu there. pub(crate) fn open_text_menu(&mut self) -> Result<(), Box> { - let Some((response, context)) = self.view.context()? else { + let Some((response, context)) = self.pane.view.context()? else { return Ok(()); }; self.respond(response); @@ -338,15 +338,15 @@ impl State { let correction = context.spelling.unwrap(); let response = match chosen_text.as_str() { _ if chosen < correction.suggestions.len() => { - self.view.correct(&correction, &chosen_text)? + self.pane.view.correct(&correction, &chosen_text)? } - "Delete Repeated Word" => self.view.correct(&correction, "")?, + "Delete Repeated Word" => self.pane.view.correct(&correction, "")?, "Spelling" => { self.open_spelling_pane(); return Ok(()); } text => { - if let Some(spelling) = &self.view.spelling { + if let Some(spelling) = &self.pane.view.spelling { if text == "Ignore" { spelling.ignore(&correction.word); } else { @@ -407,7 +407,8 @@ impl State { separated: true, disabled: disabled(commands::Id::Cut) || image.size.is_none_or(|size| { - self.view + self.pane + .view .editor .image_placement(image.id) .is_none_or(|(_, shown)| shown == size) @@ -554,7 +555,7 @@ impl State { return Ok(()); } "Restore to Original Size" => { - let response = self.view.restore_picture_size(image.id)?; + let response = self.pane.view.restore_picture_size(image.id)?; self.respond(response); return Ok(()); } @@ -581,15 +582,15 @@ impl State { return Ok(()); } "Copy Link to Paragraph" => { - if let Some(session) = &self.session { - let link = self.page_link(session.space, context.paragraph)?; + if self.session.is_some() { + let link = self.page_link(self.pane.showing.space, context.paragraph)?; self.clipboard.set_text(link)?; } return Ok(()); } - "Select Link" => self.view.unlink(true)?, - "Remove Link" => self.view.unlink(false)?, - text => self.view.switch_equation(text == "Linear")?, + "Select Link" => self.pane.view.unlink(true)?, + "Remove Link" => self.pane.view.unlink(false)?, + text => self.pane.view.switch_equation(text == "Linear")?, }; self.respond(response); Ok(()) diff --git a/crates/snowbound/src/live.rs b/crates/snowbound/src/live.rs index 83330811aa40f27925092f8e240638ef816fcd99..246a2ef876da85f53ce339103e680ce782006a00 100644 --- a/crates/snowbound/src/live.rs +++ b/crates/snowbound/src/live.rs @@ -590,8 +590,9 @@ impl State { let Some(session) = &self.session else { return live::Presence::default(); }; - let outline = self.view.editor.active_outline(); + let outline = self.pane.view.editor.active_outline(); let [anchor, focus] = self + .pane .view .editor .selection() @@ -599,7 +600,7 @@ impl State { .map(|position| spot(outline, position)); live::Presence { section: section(session), - page: Some(session.space.into()), + page: Some(self.pane.showing.space.into()), caret: anchor .zip(focus) .map(|(anchor, focus)| Caret { anchor, focus }), @@ -815,26 +816,26 @@ impl State { self.commands .push(Command::OpenSection(library, tab.path.clone())); } - } else if presence.page != Some(session.space.into()) { + } else if presence.page != Some(self.pane.showing.space.into()) { if asked != Some(place) { self.commands.push(Command::OpenPage(presence.page?.into())); } } else { self.peers.going = None; let caret = presence.caret?; - let (outline, focus) = find(&self.view.editor, caret.focus)?; + let (outline, focus) = find(&self.pane.view.editor, caret.focus)?; let rect = outline .caret_at(focus, parley::Affinity::Downstream, 0.0) .ok()?; let [x, y] = outline.origin(); - let viewport = self.view.viewport; + let viewport = self.pane.view.viewport; let [width, height] = viewport.size.map(|side| side as f32); let middle = ((rect.y0 + rect.y1) as f32 / 2.0 + y) * viewport.scale; - let mut responses = vec![self.view.scroll_to(1, middle - height / 2.0).ok()?]; + let mut responses = vec![self.pane.view.scroll_to(1, middle - height / 2.0).ok()?]; let across = (rect.x0 as f32 + x) * viewport.scale + viewport.origin[0]; if !(0.0..width).contains(&across) { let left = (rect.x0 as f32 + x) * viewport.scale - width / 2.0; - responses.push(self.view.scroll_to(0, left).ok()?); + responses.push(self.pane.view.scroll_to(0, left).ok()?); } for response in responses { self.respond(response); @@ -924,9 +925,9 @@ impl State { let Some([left, top, right, bottom]) = self.ui.laid_out(page()) else { return; }; - let here = (section(session), Some(session.space.into())); + let here = (section(session), Some(self.pane.showing.space.into())); let scale = self.ui.scale(); - let viewport = self.view.viewport; + let viewport = self.pane.view.viewport; // A point on the page in points from the page box's corner. let shown = |[x, y]: [f32; 2]| { [ @@ -943,7 +944,7 @@ impl State { else { continue; }; - let Some((outline, focus)) = find(&self.view.editor, caret.focus) else { + let Some((outline, focus)) = find(&self.pane.view.editor, caret.focus) else { continue; }; let Ok(rect) = outline.caret_at(focus, parley::Affinity::Downstream, 0.0) else { @@ -971,7 +972,7 @@ impl State { if !(0.0..right - left).contains(&x0) { continue; } - let selection = find(&self.view.editor, caret.anchor) + let selection = find(&self.pane.view.editor, caret.anchor) .filter(|(other, _)| other.id == outline.id) .and_then(|(_, anchor)| outline.range_rects([anchor, focus].into()).ok()) .unwrap_or_default() diff --git a/crates/snowbound/src/main.rs b/crates/snowbound/src/main.rs index 50e60bb26b9e24e2d0073a0f40d328ffe449b8f7..ec68bd8e73c810857a71820190b03d1ed67379e9 100644 --- a/crates/snowbound/src/main.rs +++ b/crates/snowbound/src/main.rs @@ -539,7 +539,44 @@ fn icloud_listed(proxy: EventLoopProxy) -> impl Fn() + Send + Sync + } } -/// The open section and the page the editor shows. +/// A page view, and which page, locked section or empty notebook it shows. +struct Pane { + view: PageView, + showing: Showing, + /// The newest read requested; older ones are dropped when they finish. + loading: u64, + opening: Option, + /// The section tab, if another, that the newest read opens, and when it was asked for. + switching: Option<(Option, Instant)>, + /// Pages visited, for Back and Forward. + trail: navigation::Trail, + /// The stored page changed elsewhere while marked text was pending; it is shown once + /// the composition ends. + stale: bool, + /// Whether the page last heard it had keyboard focus. + page_focused: bool, + /// The notebook shown while it has no section open, as one whose last section was + /// deleted. + sectionless: Option>, + /// A password-protected section shown locked, in place of an open one. + locked: Option, +} + +/// Which page of the open section a pane shows, and what the page list shows beneath it. +#[derive(Default)] +struct Showing { + space: ExGuid, + /// The version of page `space` open, by its context. + version: Option, + /// The page whose conflict pages the list shows beneath it. + shown: Option, + /// Which of the open conflict page's conflicting changes is selected, in page order. + change: Option, + /// The page whose versions the list shows beneath it (Show Page Versions). + shown_history: Option, +} + +/// An open section. struct Session { section: notebook::session::Section, /// The notebook the section belongs to. @@ -549,21 +586,12 @@ struct Session { tab: usize, /// Spaces, titles and outline levels in section order. pages: Vec<(ExGuid, String, u32)>, - space: ExGuid, /// The section's sync status as last read. sync: notebook::session::SyncStatus, /// Each page's conflict pages, the versions a merge could not take. conflicts: Vec<(ExGuid, Vec)>, - /// The page whose conflict pages the list shows beneath it. - shown: Option, - /// Which of the open conflict page's conflicting changes is selected, in page order. - change: Option, /// Each page's versions, newest first. history: Vec<(ExGuid, Vec)>, - /// The page whose versions the list shows beneath it (Show Page Versions). - shown_history: Option, - /// The version of page `space` open, by its context. - version: Option, } /// The information bar above a page with conflict pages, or above one of them. @@ -602,13 +630,13 @@ impl Session { Ok(()) } - fn title(&self) -> &str { - match self.conflict(self.space) { + fn title(&self, space: ExGuid) -> &str { + match self.conflict(space) { Some((_, version)) => &version.title, None => self .pages .iter() - .find(|(space, ..)| *space == self.space) + .find(|(listed, ..)| *listed == space) .map_or("", |(_, title, _)| title), } } @@ -630,33 +658,34 @@ impl Session { }) } - fn bar(&self) -> Option { - if let Some(version) = self.version { + fn bar(&self, showing: &Showing) -> Option { + let space = showing.space; + if let Some(version) = showing.version { return Some(Bar::History { - page: self.space, + page: space, version, grouped: self.grouped(), }); } - match self.conflict(self.space) { + match self.conflict(space) { Some((page, _)) => Some(Bar::Version { page, - version: self.space, + version: space, }), - None => (!self.versions(self.space).is_empty()).then_some(Bar::Page { - page: self.space, - shown: self.shown == Some(self.space), + None => (!self.versions(space).is_empty()).then_some(Bar::Page { + page: space, + shown: showing.shown == Some(space), }), } } - fn refresh_conflicts(&mut self) -> Result<(), Box> { + fn refresh_conflicts(&mut self, showing: &mut Showing) -> Result<(), Box> { self.conflicts = self.section.conflicts()?; - if self + if showing .shown .is_some_and(|page| self.versions(page).is_empty()) { - self.shown = None; + showing.shown = None; } Ok(()) } @@ -685,9 +714,9 @@ impl Session { /// section, or a notebook read again after a change, with the catalog path the open /// section has in it. enum Loaded { - Section(Box, Page), + Section(Box, ExGuid, Page), /// A section just created, whose name opens for renaming as OneNote's New Section does. - Created(Box, Page), + Created(Box, ExGuid, Page), Page(ExGuid, Page), /// A page's version, by the page's space and the version's context. Version(ExGuid, ExGuid, Page), @@ -697,8 +726,8 @@ enum Loaded { /// What an opening page replaces: the section, or the page of the open one. enum Shown { - Section(Box), - Created(Box), + Section(Box, ExGuid), + Created(Box, ExGuid), Page(ExGuid), Version(ExGuid, ExGuid), } @@ -822,19 +851,14 @@ struct State { loads: (mpsc::Sender, mpsc::Receiver), /// Lays out pages the loader threads read, off the frame. layouts: Arc>, - /// The newest read requested; older ones are dropped when they finish. - loading: u64, - opening: Option, - /// The section tab, if another, that the newest read opens, and when it was asked for. - switching: Option<(Option, Instant)>, prefetch: prefetch::Prefetch, /// Where each page was left this run, by `Library::key` and page space, as OneNote /// returns to it until the notebook closes. places: HashMap<(String, ExGuid), Place>, /// The page each section showed last this run, by `Library::key`. last_pages: HashMap, - /// Pages visited, for Back and Forward. - trail: navigation::Trail, + /// Each page shown lately once, latest first, kept between launches. + recent: Vec, /// What Undo and Redo take back across pages and sections. undo: undo::Timeline, swipe: navigation::Swipe, @@ -853,9 +877,11 @@ struct State { /// Backends that failed to start this run, and why. unavailable: Vec<(settings::Backend, String)>, ui: Ui, - view: PageView, + /// The page view the window shows. + pane: Pane, /// The open notebooks in the sidebar's order. notebooks: Vec>, + /// The open section, the one the pane shows. session: Option, /// A page not kept in any notebook, from a text file or `--page`. temporary: bool, @@ -938,11 +964,6 @@ struct State { titlebar: [[f32; 4]; 2], /// A section or group being renamed in the sidebar. renaming: Option, - /// The notebook shown while it has no section open, as one whose last section was - /// deleted. - sectionless: Option>, - /// A password-protected section shown locked, in place of an open one. - locked: Option, /// The password dialog while it is open. password: Option, /// Options' Passwords. @@ -977,11 +998,6 @@ struct State { changed: bool, /// Only the view scrolled or zoomed during this frame, to be announced after it. moved: bool, - /// The stored page changed elsewhere while marked text was pending; it is shown once - /// the composition ends. - stale: bool, - /// Whether the page last heard it had keyboard focus. - page_focused: bool, /// The pointer in logical pixels, for window drags from the strip. pointer: [f32; 2], /// When the strip was last pressed, to zoom on a double click. @@ -1180,6 +1196,7 @@ impl State { icloud::look_up(icloud_listed(proxy.clone())); let mut notebooks = Vec::new(); let mut session = None; + let mut showing = Showing::default(); let mut sectionless = None; let mut locked = None; let (editor, scene) = match input { @@ -1213,9 +1230,10 @@ impl State { .0; notebooks.push(Arc::clone(&library)); let path = library.location.clone(); - let (opened, page) = read_session(section, library, path, Some(space))?; + let (opened, space, page) = read_session(section, library, path, Some(space))?; let (scene, editor) = PageScene::from_page(page, &mut engine)?; session = Some(opened); + showing.space = space; (editor, Some((scene, [0.0; 2]))) } Input::Notebooks { locations, current } => { @@ -1249,10 +1267,11 @@ impl State { match shown { Some((library, path, left)) if !library.locked(&path) => { let section = library.open_async(&path, notify(proxy.clone())).await?; - let (opened, page) = + let (opened, space, page) = read_session(section, Arc::clone(library), path, left)?; let (scene, editor) = PageScene::from_page(page, &mut engine)?; session = Some(opened); + showing.space = space; (editor, Some((scene, [0.0; 2]))) } shown => { @@ -1348,14 +1367,25 @@ impl State { drawing, unavailable, ui, - view: PageView::new( - editor, - engine, - scene, - [size.width, size.height], - dpr, - platform::double_click_interval(), - ), + pane: Pane { + view: PageView::new( + editor, + engine, + scene, + [size.width, size.height], + dpr, + platform::double_click_interval(), + ), + showing, + loading: 0, + opening: None, + switching: None, + trail: navigation::Trail::default(), + stale: false, + page_focused: true, + sectionless, + locked, + }, notebooks, session, temporary, @@ -1398,8 +1428,6 @@ impl State { light_pages: stored.light_pages, titlebar, renaming: None, - sectionless, - locked, password: None, passwords: stored.passwords, notebook_theme: (stored.notebook_theme.0) @@ -1420,8 +1448,6 @@ impl State { commands: Vec::new(), changed: false, moved: false, - stale: false, - page_focused: true, pointer: [0.0; 2], strip_press: None, strip_held: false, @@ -1439,18 +1465,11 @@ impl State { loads: mpsc::channel(), prefetch, layouts, - loading: 0, - opening: None, - switching: None, places: HashMap::new(), last_pages: (stored.recent.iter().rev()) .map(|place| (library::key(&place.notebook, &place.section), place.page)) .collect(), - trail: { - let mut trail = navigation::Trail::default(); - trail.recent = stored.recent; - trail - }, + recent: stored.recent, undo: undo::Timeline::default(), swipe: navigation::Swipe::default(), access_adapter, @@ -1464,9 +1483,9 @@ impl State { servers: stored.servers, correction: None, }; - state.view.snap_to_grid = !stored.ignore_grid; - state.view.editor.default_font = stored.default_font; - state.view.editor.markdown = commands::markdown(!stored.ignore_markdown); + state.pane.view.snap_to_grid = !stored.ignore_grid; + state.pane.view.editor.default_font = stored.default_font; + state.pane.view.editor.markdown = commands::markdown(!stored.ignore_markdown); // A notebook opened from its server that couldn't sign in asks to, as the Finder does. let unsigned = state.notebooks.iter().find(|library| { library.notebook.is_err() && library::server_address(&library.location).is_some() @@ -1557,7 +1576,7 @@ impl State { let scale = self.scale(); if scale != self.ui.scale() { self.renderer.clear_glyph_cache(); - let response = self.view.scale_factor_changed(scale)?; + let response = self.pane.view.scale_factor_changed(scale)?; self.respond(response); } #[cfg(feature = "live")] @@ -1572,10 +1591,10 @@ impl State { platform::resize_direction(&self.window, self.pointer) .map(CursorIcon::from) .or_else(|| self.ui.cursor()) - .unwrap_or_else(|| cursor_icon(self.view.cursor())), + .unwrap_or_else(|| cursor_icon(self.pane.view.cursor())), ); let ime = if self.ui.focused() == Some(page()) { - self.view.accepts_text() + self.pane.view.accepts_text() } else { search::takes_text(self.ui.focused()) }; @@ -1590,8 +1609,8 @@ impl State { let commands = std::mem::take(&mut self.commands); let follow = !commands.is_empty() || self.ui.wants_frame() - || self.opening.is_some() - || self.switching.is_some(); + || self.pane.opening.is_some() + || self.pane.switching.is_some(); for command in commands { self.apply(command)?; } @@ -1601,7 +1620,7 @@ impl State { self.after_move()?; } self.moved = false; - if self.stale { + if self.pane.stale { self.refresh()?; } // A covered window shows nothing, so animations wait for it to be uncovered. @@ -1669,8 +1688,8 @@ impl State { ((rect[2] - rect[0]) * scale).round() as u32, ((rect[3] - rect[1]) * scale).round() as u32, ]; - if size != self.view.viewport.size { - let response = self.view.resized(size)?; + if size != self.pane.view.viewport.size { + let response = self.pane.view.resized(size)?; self.respond(response); } } @@ -1743,8 +1762,8 @@ impl State { platform::update_menu(|| self.statuses()); let welcome = self.session.is_none() && !self.temporary - && self.sectionless.is_none() - && self.locked.is_none(); + && self.pane.sectionless.is_none() + && self.pane.locked.is_none(); self.toolbar(&theme, !welcome)?; if welcome { self.welcome(); @@ -1916,7 +1935,7 @@ impl State { } (clicked, open_tab) } - None if self.locked.is_some() => self.locked_tabs(row, §ion, theme.strip), + None if self.pane.locked.is_some() => self.locked_tabs(row, §ion, theme.strip), None => { let tabs = [("Temporary page", section_color(None), false)]; let shown = if self.temporary { &tabs[..] } else { &[] }; @@ -1994,13 +2013,16 @@ impl State { }, ); if let Some(session) = &self.session - && let Some(bar) = session.bar() + && let Some(bar) = session.bar(&self.pane.showing) { let changes = self.changes().len(); let sections: Vec<&str> = session.tabs.iter().map(|tab| tab.name.as_str()).collect(); let steps = [ - session.change.is_some_and(|at| at > 0), - session.change.map_or(changes > 0, |at| at + 1 < changes), + self.pane.showing.change.is_some_and(|at| at > 0), + self.pane + .showing + .change + .map_or(changes > 0, |at| at + 1 < changes), ]; self.commands .extend(conflict_bar(&mut self.ui, bar, §ions, steps)); @@ -2008,7 +2030,7 @@ impl State { if self.in_recycle_bin() { recycle::bar(&mut self.ui); } - if self.session.is_none() && self.locked.is_some() { + if self.session.is_none() && self.pane.locked.is_some() { self.locked_page(); self.ui.close(); self.ui.close(); @@ -2021,7 +2043,12 @@ impl State { self.context_menu(); return Ok((section, open_tab, None)); } - if let Some(library) = self.sectionless.clone().filter(|_| self.session.is_none()) { + if let Some(library) = self + .pane + .sectionless + .clone() + .filter(|_| self.session.is_none()) + { self.no_sections(library); self.ui.close(); self.ui.close(); @@ -2053,7 +2080,7 @@ impl State { self.template_strip(&theme); } self.transport(&theme)?; - let scroll = self.view.scroll(); + let scroll = self.pane.view.scroll(); for (index, axis) in [Axis::X, Axis::Y] .into_iter() .filter(|_| !opening) @@ -2063,13 +2090,13 @@ impl State { &mut self.ui, index, axis, - -self.view.viewport.origin[index], + -self.pane.view.viewport.origin[index], [scroll.min[index], scroll.max[index]], - self.view.viewport.size[index] as f32, + self.pane.view.viewport.size[index] as f32, scroll.max[1 - index] > scroll.min[1 - index], [0.18, 0.18, 0.18, 0.45], ) { - let response = self.view.scroll_to(index, offset)?; + let response = self.pane.view.scroll_to(index, offset)?; self.respond(response); } } @@ -2077,7 +2104,7 @@ impl State { if !opening { self.peer_carets(); } - let task = self.view.task_under_pointer().map(|[x0, y0, x1, y1]| { + let task = self.pane.view.task_under_pointer().map(|[x0, y0, x1, y1]| { let scale = self.ui.scale(); self.ui.leaf( "task", @@ -2101,9 +2128,9 @@ impl State { } let signal = self.ui.signal(page()); let focused = self.ui.window_focused && self.ui.focused() == Some(page()); - if focused != self.page_focused { - self.page_focused = focused; - let response = self.view.focus_changed(focused)?; + if focused != self.pane.page_focused { + self.pane.page_focused = focused; + let response = self.pane.view.focus_changed(focused)?; self.respond(response); } self.page_events(signal.events)?; @@ -2427,7 +2454,7 @@ impl State { let fonts = &self.fonts; let recent_fonts = &self.recent_fonts; let pens = &self.toolbar; - let engine = &self.view.engine; + let engine = &self.pane.view.engine; let session = self.session.as_ref(); let update = self.updates.status(); let sheet = self.gallery_sheet(); @@ -3126,8 +3153,8 @@ impl State { color: ui.theme.paper, ink: ui.theme.paper_ink, }, - self.view.editor.page_color(), - self.view.editor.rule_lines(), + self.pane.view.editor.page_color(), + self.pane.view.editor.rule_lines(), false, &mut self.page_color_preview, ) @@ -3187,7 +3214,7 @@ impl State { let format = self.format_state(); let [out, into] = [Cmd::ZoomOut, Cmd::ZoomIn].map(|id| self.status(&Choice::Command(id), &format)); - let level = format!("{:.0}%", self.view.zoom() * 100.0); + let level = format!("{:.0}%", self.pane.view.zoom() * 100.0); let field = self.ui.id("zoom field"); if self.ui.focused() != Some(field) { self.zoom_typed = None; @@ -3303,7 +3330,7 @@ impl State { self.choose(choice); } if let Some(percent) = zoom { - let response = self.view.set_zoom(percent / 100.0)?; + let response = self.pane.view.set_zoom(percent / 100.0)?; self.respond(response); } Ok(()) @@ -3402,7 +3429,7 @@ impl State { node.set_orientation(accesskit::Orientation::Vertical); } // Another section's pages are on their way. - if self.loading().is_some() && self.switching.is_some_and(|(tab, _)| tab.is_some()) { + if self.loading().is_some() && self.pane.switching.is_some_and(|(tab, _)| tab.is_some()) { self.ui.close(); return None; } @@ -3419,6 +3446,7 @@ impl State { theme, section, session, + &self.pane.showing, [&found, &unread], &peers, (rounding, self.page_tabs_left), @@ -3461,7 +3489,10 @@ impl State { if self.loading().is_some() { return Ok(()); } - let read_only = self.session.as_ref().is_some_and(Session::read_only); + let read_only = self + .session + .as_ref() + .is_some_and(|session| session.read_only(&self.pane.showing)); let scale = self.ui.scale(); let corner = self.ui.laid_out(page()).unwrap_or_default(); let device = |point: [f32; 2]| { @@ -3472,30 +3503,32 @@ impl State { }; for event in events { let response = match event { - ui::Event::PointerMoved(point) => self.view.pointer_moved(device(point))?, + ui::Event::PointerMoved(point) => self.pane.view.pointer_moved(device(point))?, ui::Event::Pressure(pressure) => { - self.view + self.pane + .view .set_pressure(pressure.filter(|_| self.pen_pressure)); continue; } - ui::Event::PointerLeft => self.view.pointer_left(), + ui::Event::PointerLeft => self.pane.view.pointer_left(), ui::Event::Button { button: MouseButton::Left, pressed, at, } => { if pressed { - let response = self.view.pointer_pressed(at)?; + let response = self.pane.view.pointer_pressed(at)?; // Drawing or clicking anywhere but the title puts the templates away. - if (self.view.inking() || !self.view.editor.active_outline().title) - && let Some(session) = &self.session + if (self.pane.view.inking() + || !self.pane.view.editor.active_outline().title) + && self.session.is_some() { - self.dismissed.insert(session.space); + self.dismissed.insert(self.pane.showing.space); } response } else { - let response = self.view.pointer_released()?; - let [anchor, focus] = self.view.editor.selection().positions; + let response = self.pane.view.pointer_released()?; + let [anchor, focus] = self.pane.view.editor.selection().positions; if anchor != focus && let Some(painted) = self.painter.take() { @@ -3521,7 +3554,7 @@ impl State { ui::Event::Ime(Ime::Preedit(..) | Ime::Commit(_)) if read_only => continue, ui::Event::Key { ref key, .. } if read_only - && !self.view.modifiers().command + && !self.pane.view.modifiers().command && !matches!( key, Key::Named( @@ -3538,20 +3571,25 @@ impl State { { continue; } - ui::Event::Wheel(delta) => self.view.wheel([delta[0] * scale, delta[1] * scale])?, + ui::Event::Wheel(delta) => { + self.pane.view.wheel([delta[0] * scale, delta[1] * scale])? + } ui::Event::Key { key, text } => { - self.view.key(&ui::edit_key(&key), text.as_deref())? + self.pane.view.key(&ui::edit_key(&key), text.as_deref())? } - ui::Event::Ime(Ime::Preedit(text, cursor)) => self.view.compose(text, cursor)?, - ui::Event::Ime(Ime::Commit(text)) => self.view.commit_text(text)?, - ui::Event::Ime(Ime::Disabled) => self.view.cancel_composition()?, - ui::Event::Modifiers(modifiers) => { - self.view.modifiers_changed(ui::edit_modifiers(modifiers))? + ui::Event::Ime(Ime::Preedit(text, cursor)) => { + self.pane.view.compose(text, cursor)? } + ui::Event::Ime(Ime::Commit(text)) => self.pane.view.commit_text(text)?, + ui::Event::Ime(Ime::Disabled) => self.pane.view.cancel_composition()?, + ui::Event::Modifiers(modifiers) => self + .pane + .view + .modifiers_changed(ui::edit_modifiers(modifiers))?, }; self.respond(response); } - if self.view.editor.marked_range().is_none() { + if self.pane.view.editor.marked_range().is_none() { platform::clear_marked_text(&self.window); } Ok(()) @@ -3579,7 +3617,7 @@ impl State { let tab = match &self.session { // Back to the open section before another one opened. Some(session) - if self.switching.is_some() + if self.pane.switching.is_some() && Arc::ptr_eq(&session.library, &library) && session.tabs[session.tab].path == path => { @@ -3591,34 +3629,37 @@ impl State { } _ => None, }; - self.switching = Some((tab, Instant::now())); + self.pane.switching = Some((tab, Instant::now())); let notify = notify(self.proxy.clone()); let last = self.last_pages.get(&library.key(&path)).copied(); self.load_async(move || async move { let start = Instant::now(); let section = library.open_async(&path, notify).await?; lap("switch section open", start); - let (session, page) = read_session(section, library, path, last)?; - Ok(Loaded::Section(Box::new(session), page)) + let (session, space, page) = read_session(section, library, path, last)?; + Ok(Loaded::Section(Box::new(session), space, page)) }); } Command::OpenPage(space) => { let session = self.session.as_ref().ok_or("No section is open")?; - if self.switching.is_some() && session.space == space && session.version.is_none() { + if self.pane.switching.is_some() + && self.pane.showing.space == space + && self.pane.showing.version.is_none() + { self.stop_loading(); return Ok(()); } - self.switching = Some((None, Instant::now())); + self.pane.switching = Some((None, Instant::now())); let read = session.reader(space); self.load(move || Ok(Loaded::Page(space, read()?))); } Command::Versions { page, show } => { let session = self.session.as_mut().ok_or("No section is open")?; - session.shown = show.then_some(page); + self.pane.showing.shown = show.then_some(page); let open = if show { session.versions(page).first().map(|version| version.space) } else { - (session.space != page).then_some(page) + (self.pane.showing.space != page).then_some(page) }; self.commands.extend(open.map(Command::OpenPage)); } @@ -3627,7 +3668,7 @@ impl State { let operation = session.section.delete_pages_async(&[version]); self.section_edit(operation, move |state, _| { if let Some(session) = &mut state.session { - session.refresh_conflicts()?; + session.refresh_conflicts(&mut state.pane.showing)?; } state.commands.push(Command::OpenPage(page)); Ok(()) @@ -3635,9 +3676,9 @@ impl State { } Command::CopyVersion { version, tab } => { let session = self.session.as_mut().ok_or("No section is open")?; - let page = match session.version { + let page = match self.pane.showing.version { Some(open) if open == version => { - session.section.version(session.space, open)? + session.section.version(self.pane.showing.space, open)? } _ => session.section.page(version)?, }; @@ -3729,7 +3770,7 @@ impl State { Command::Choose(choice) => self.run(choice)?, Command::TagPicture => self.pick_tag_picture(), Command::Text(text) => { - if let Some(context) = self.view.caret_context() { + if let Some(context) = self.pane.view.caret_context() { self.text_command(context, text)?; } } @@ -3747,8 +3788,8 @@ impl State { &mut self, read: impl FnOnce() -> F + Send + 'static, ) { - self.loading += 1; - let (id, sender, redraw) = (self.loading, self.loads.0.clone(), self.redraw.clone()); + self.pane.loading += 1; + let (id, sender, redraw) = (self.pane.loading, self.loads.0.clone(), self.redraw.clone()); let layouts = Arc::clone(&self.layouts); let requested = Instant::now(); let scenes = Arc::clone(&self.prefetch.scenes); @@ -3757,8 +3798,8 @@ impl State { let laid = read().await.and_then(|loaded| { lap("switch read", requested); let (shown, page) = match loaded { - Loaded::Section(session, page) => (Shown::Section(session), page), - Loaded::Created(session, page) => (Shown::Created(session), page), + Loaded::Section(session, space, page) => (Shown::Section(session, space), page), + Loaded::Created(session, space, page) => (Shown::Created(session, space), page), Loaded::Page(space, page) => (Shown::Page(space), page), Loaded::Version(space, version, page) => (Shown::Version(space, version), page), Loaded::Library(library, path) => return Ok(Laid::Library(library, path)), @@ -3766,8 +3807,8 @@ impl State { let mut engine = layouts.lock().map_err(|_| "Page layout failed")?; let start = Instant::now(); let key = match &shown { - Shown::Section(session) | Shown::Created(session) => { - Some((session.key(), session.space)) + Shown::Section(session, space) | Shown::Created(session, space) => { + Some((session.key(), *space)) } Shown::Page(space) => open.map(|open| (open, *space)), Shown::Version(..) => None, @@ -3846,12 +3887,12 @@ impl State { for (id, loaded) in reads { match loaded { // A later load supersedes what this one shows, but not its failure. - Ok(_) if id != self.loading => {} - Ok(Laid::Page(opening)) => self.opening = Some(*opening), + Ok(_) if id != self.pane.loading => {} + Ok(Laid::Page(opening)) => self.pane.opening = Some(*opening), Ok(Laid::Library(library, path)) => self.adopt(library, path.as_deref())?, Err(error) => { - if id == self.loading { - self.switching = None; + if id == self.pane.loading { + self.pane.switching = None; } platform::alert("Couldn't open", &error); } @@ -3861,17 +3902,18 @@ impl State { color: self.ui.theme.paper, ink: self.ui.theme.paper_ink, }; - self.prefetch.draw(&self.view, paper, &self.redraw); - let Some(mut opening) = self.opening.take() else { + self.prefetch.draw(&self.pane.view, paper, &self.redraw); + let Some(mut opening) = self.pane.opening.take() else { return Ok(()); }; let paper = paper.colored(opening.editor.page_color()); if !self + .pane .view .prepare(&mut opening.scene, &opening.editor, paper, &self.redraw) && opening.since.elapsed() < HOLD { - self.opening = Some(opening); + self.pane.opening = Some(opening); return Ok(()); } lap("switch pictures", opening.since); @@ -3880,7 +3922,7 @@ impl State { fn show_page(&mut self, mut opening: Opening) -> Result<(), Box> { let requested = opening.requested; - if let Shown::Section(session) | Shown::Created(session) = &mut opening.loaded + if let Shown::Section(session, _) | Shown::Created(session, _) = &mut opening.loaded && let Some(library) = self .notebooks .iter() @@ -3905,7 +3947,7 @@ impl State { } else if let Some(path) = library.first_section() { return self.apply(Command::OpenSection(library, path)); } else { - self.switching = None; + self.pane.switching = None; return self.adopt(library, None); } } @@ -3916,22 +3958,24 @@ impl State { let mut leaving = self .session .as_ref() - .filter(|session| !session.read_only()) - .map(|session| session.space); + .filter(|session| !session.read_only(&self.pane.showing)) + .map(|_| self.pane.showing.space); if let Some(session) = &self.session { let key = session.key(); - self.places - .insert((key.clone(), session.space), self.view.place()); - self.last_pages.insert(key.clone(), session.space); - if session.version.is_none() - && let Some(scene) = self.view.scene.take() + self.places.insert( + (key.clone(), self.pane.showing.space), + self.pane.view.place(), + ); + self.last_pages.insert(key.clone(), self.pane.showing.space); + if self.pane.showing.version.is_none() + && let Some(scene) = self.pane.view.scene.take() { - self.prefetch.keep(key, session.space, scene); + self.prefetch.keep(key, self.pane.showing.space, scene); } } - let created = matches!(opening.loaded, Shown::Created(_)); + let created = matches!(opening.loaded, Shown::Created(..)); match opening.loaded { - Shown::Section(session) | Shown::Created(session) => { + Shown::Section(session, space) | Shown::Created(session, space) => { // The settings keep the notebook shown, not the section. let other_notebook = self .session @@ -3940,7 +3984,7 @@ impl State { self.list(&session.library); // The section left stays open a while for coming back to; the notebook's // background sync takes it over once it is closed. - self.locked = None; + self.pane.locked = None; if let Some(previous) = self.session.replace(*session) { let path = &previous.tabs[previous.tab].path; let locked = self.leave(&previous.library, path); @@ -3956,7 +4000,11 @@ impl State { }); } } - self.sectionless = None; + self.pane.showing = Showing { + space, + ..Showing::default() + }; + self.pane.sectionless = None; if other_notebook { self.save_settings(); } @@ -3964,12 +4012,12 @@ impl State { Shown::Page(space) | Shown::Version(space, _) => { let session = self.session.as_mut().ok_or("No section is open")?; let pages = session.section.pages()?; - session.space = space; - session.version = match opening.loaded { + self.pane.showing.space = space; + self.pane.showing.version = match opening.loaded { Shown::Version(_, version) => Some(version), _ => None, }; - session.change = None; + self.pane.showing.change = None; // Pages created, moved or deleted since the list was read. session.pages = pages; leaving = leaving.filter(|left| session.pages.iter().any(|(s, ..)| s == left)); @@ -3977,9 +4025,12 @@ impl State { } let place = self.session.as_ref().and_then(|session| { let key = session.key(); - self.places.get(&(key, session.space)).copied() + self.places.get(&(key, self.pane.showing.space)).copied() }); - let left = self.view.open(opening.editor, Some(opening.scene), place); + let left = self + .pane + .view + .open(opening.editor, Some(opening.scene), place); // Each page takes up the history it was left with. if let Some(leaving) = leaving { self.undo.park(leaving, left); @@ -3987,19 +4038,19 @@ impl State { let shown = self .session .as_ref() - .filter(|session| !session.read_only()) - .map(|session| session.space); + .filter(|session| !session.read_only(&self.pane.showing)) + .map(|_| self.pane.showing.space); if let Some(parked) = shown.and_then(|space| self.undo.resume(space)) { - self.view.resume(parked)?; + self.pane.view.resume(parked)?; } lap("switch shown", requested); - self.switching = None; + self.pane.switching = None; self.visited(); if let Some((space, action)) = self.after_open.take() && let Some(session) = self .session .as_ref() - .filter(|session| session.space == space) + .filter(|_| self.pane.showing.space == space) { let target = menus::Target::Page { library: Arc::clone(&session.library), @@ -4029,7 +4080,7 @@ impl State { self.list(&library); let shown = |shown: &Library| shown.location == library.location; let Some(path) = path else { - if self.sectionless.as_deref().is_some_and(shown) + if self.pane.sectionless.as_deref().is_some_and(shown) || self .session .as_ref() @@ -4043,7 +4094,7 @@ impl State { } }); } - self.sectionless = Some(Arc::clone(&library)); + self.pane.sectionless = Some(Arc::clone(&library)); self.title(); } self.show_arrived(&library); @@ -4104,7 +4155,7 @@ impl State { fn after_move(&mut self) -> Result<(), Box> { let scale = self.ui.scale(); let corner = self.ui.laid_out(page()).unwrap_or_default(); - let [x0, y0, x1, y1] = self.view.caret_area()?; + let [x0, y0, x1, y1] = self.pane.view.caret_area()?; self.window.set_ime_cursor_area( LogicalPosition::new(x0 / scale + corner[0], y0 / scale + corner[1]), LogicalSize::new((x1 - x0) / scale, (y1 - y0) / scale), @@ -4132,11 +4183,11 @@ impl State { } else { "" }; - format!("{}{bin} · {file}", session.title()) + format!("{}{bin} · {file}", session.title(self.pane.showing.space)) } None if !self.temporary => "Snowbound".to_owned(), None => { - let editor = &self.view.editor; + let editor = &self.pane.view.editor; if !editor.active_outline().title { return; } @@ -4169,27 +4220,27 @@ impl State { } fn edit_date(&mut self, field: DateField) { - let Some(date) = self.view.editor.date() else { + let Some(date) = self.pane.view.editor.date() else { return; }; let timestamp = date.timestamp(); - self.view.editor.finish_composition(); + self.pane.view.editor.finish_composition(); platform::clear_marked_text(&self.window); let title = match field { DateField::Date => "Change Page Date", DateField::Time => "Change Page Time", }; - let page = self.session.as_ref().map(|session| session.space); + let page = self.session.as_ref().map(|_| self.pane.showing.space); let reply = self.reply( move |state, chosen: Result<(u64, [String; 2]), &'static str>| { let (changed, text) = chosen?; // The answer belongs to the page that asked, unless another has opened since. - if state.session.as_ref().map(|session| session.space) != page - || state.view.editor.date().map(|date| date.timestamp()) != Some(timestamp) + if state.session.as_ref().map(|_| state.pane.showing.space) != page + || state.pane.view.editor.date().map(|date| date.timestamp()) != Some(timestamp) { return Ok(()); } - let response = state.view.change_date(changed, text)?; + let response = state.pane.view.change_date(changed, text)?; state.respond(response); Ok(()) }, @@ -4211,13 +4262,13 @@ impl State { let Some((_, version)) = self .session .as_ref() - .and_then(|session| session.conflict(session.space)) + .and_then(|session| session.conflict(self.pane.showing.space)) else { return Vec::new(); }; let marked = |id: &ExGuid| version.objects.contains(id); let mut changes = Vec::new(); - for outline in self.view.editor.outlines() { + for outline in self.pane.view.editor.outlines() { for (at, node) in outline.document().text_nodes().enumerate() { let Some(text) = node.text() else { continue; @@ -4234,10 +4285,10 @@ impl State { /// Selects the next or previous conflicting change, as OneNote's conflict menu does. fn select_change(&mut self, forward: bool) -> Result<(), Box> { let changes = self.changes(); - let Some(session) = &mut self.session else { + if self.session.is_none() { return Ok(()); - }; - let next = match (session.change, forward) { + } + let next = match (self.pane.showing.change, forward) { (None, true) => 0, (Some(at), true) => at + 1, (Some(at), false) if at > 0 => at - 1, @@ -4246,12 +4297,12 @@ impl State { let Some(&(outline, paragraph, length)) = changes.get(next) else { return Ok(()); }; - session.change = Some(next); + self.pane.showing.change = Some(next); let at = |offset| canvas::document::TextPosition { paragraph, offset }; - self.view.editor.focus_outline(outline)?; - self.view.editor.select([at(0), at(length)].into())?; + self.pane.view.editor.focus_outline(outline)?; + self.pane.view.editor.select([at(0), at(length)].into())?; self.ui.set_focus(Some(page())); - let response = self.view.focus_text()?; + let response = self.pane.view.focus_text()?; self.respond(response); self.window.request_redraw(); Ok(()) @@ -4281,7 +4332,7 @@ impl State { /// Hands the section the ops the editor recorded since the last call, as one edit; the /// section thread stores it. fn persist(&mut self) -> Result<(), Box> { - let ops = match self.view.editor.take_ops() { + let ops = match self.pane.view.editor.take_ops() { Ok(ops) => ops, Err(error) => return self.refused(&error.to_string()), }; @@ -4291,12 +4342,12 @@ impl State { if ops.is_empty() { return Ok(()); } - if session.read_only() { + if session.read_only(&self.pane.showing) { // A conflict page or a version is read-only: it returns to what is stored. - self.stale = true; + self.pane.stale = true; return Ok(()); } - let space = session.space; + let space = self.pane.showing.space; session.section.apply( &self.author, onestore::op::Edit { @@ -4308,18 +4359,20 @@ impl State { }, )?; // The page list shows the title as it is typed, even when the caret left it since. - let editor = &self.view.editor; + let editor = &self.pane.view.editor; let title = (editor.outlines().iter()) .find(|outline| outline.title) .and_then(|outline| outline.document().paragraphs().last()) .map(|paragraph| onestore::op::title_line(paragraph.text())); if editor.active_outline().title - || title.is_some_and(|title| !title.is_empty() && title != session.title()) + || title.is_some_and(|title| { + !title.is_empty() && title != session.title(self.pane.showing.space) + }) { session.pages = session.section.pages()?; } self.edited(vec![space]); - let editor = &self.view.editor; + let editor = &self.pane.view.editor; self.undo .edited(space, editor.history_depth(), editor.typing().is_some()); Ok(()) @@ -4328,17 +4381,17 @@ impl State { /// Shows the page as stored after a change made elsewhere, in place: the caret, /// selection, scroll and drawn pictures stay. Waits for marked text to end. fn refresh(&mut self) -> Result<(), Box> { - if self.view.editor.marked_range().is_some() { - self.stale = true; + if self.pane.view.editor.marked_range().is_some() { + self.pane.stale = true; return Ok(()); } - self.stale = false; + self.pane.stale = false; self.persist()?; let Some(session) = &self.session else { return Ok(()); }; - let page = session.reader(session.space)()?; - let response = self.view.refresh(page)?; + let page = session.reader(self.pane.showing.space)()?; + let response = self.pane.view.refresh(page)?; self.respond(response); self.title(); self.window.request_redraw(); @@ -4349,10 +4402,13 @@ impl State { /// text the editor showed goes on the clipboard so nothing typed is lost unannounced. fn refused(&mut self, error: &str) -> Result<(), Box> { eprintln!("Saving failed: {error}"); - self.clipboard.set_text(page_text(&self.view.editor))?; + self.clipboard.set_text(page_text(&self.pane.view.editor))?; // What the editor recorded since builds on the refused edit. - let _ = self.view.editor.take_ops(); - self.view.editor.cancel_composition(&mut self.view.engine)?; + let _ = self.pane.view.editor.take_ops(); + self.pane + .view + .editor + .cancel_composition(&mut self.pane.view.engine)?; platform::clear_marked_text(&self.window); self.refresh()?; platform::alert( @@ -4413,13 +4469,13 @@ impl State { match event { Event::Changed(spaces) => { listed = true; - changed |= spaces.contains(&session.space); + changed |= spaces.contains(&self.pane.showing.space); elsewhere.extend_from_slice(&spaces); self.search .changed(session.key(), session.section.replica(), spaces); } Event::Rejected { spaces, error } => { - if spaces.contains(&session.space) { + if spaces.contains(&self.pane.showing.space) { rejected = Some(error); } else { eprintln!("Saving failed: {error}"); @@ -4447,23 +4503,26 @@ impl State { return Ok(()); }; if listed { - let back = session.shown; + let back = self.pane.showing.shown; session.pages = session.section.pages()?; - session.refresh_conflicts()?; - session.refresh_history()?; + session.refresh_conflicts(&mut self.pane.showing)?; + session.refresh_history(&mut self.pane.showing)?; let listed = |space: ExGuid| session.pages.iter().any(|(page, ..)| *page == space); - let gone = session.version.is_some_and(|version| { + let gone = self.pane.showing.version.is_some_and(|version| { !session - .page_versions(session.space) + .page_versions(self.pane.showing.space) .iter() .any(|listed| listed.context == version) }); - if gone && listed(session.space) { + if gone && listed(self.pane.showing.space) { // A version deleted elsewhere returns to its page. - self.commands.push(Command::OpenPage(session.space)); + self.commands + .push(Command::OpenPage(self.pane.showing.space)); return Ok(()); } - if !listed(session.space) && session.conflict(session.space).is_none() { + if !listed(self.pane.showing.space) + && session.conflict(self.pane.showing.space).is_none() + { // A page removed elsewhere, or a conflict page deleted elsewhere, which // returns to its page. let open = back @@ -4511,7 +4570,7 @@ impl State { } let mut error = None; let mut sent = false; - let (view, ui, window) = (&self.view, &self.ui, &self.window); + let (view, ui, window) = (&self.pane.view, &self.ui, &self.window); self.access_adapter.update_if_active(|| { sent = true; page_tree(view, ui, window, &mut self.accessibility).unwrap_or_else(|failure| { @@ -4592,7 +4651,7 @@ impl State { let mut tree = Tree::new(chrome, true); if self.ui.laid_out(page()).is_some() { let mut fresh = accessibility::Accessibility::default(); - let page = page_tree(&self.view, &self.ui, &self.window, &mut fresh)?; + let page = page_tree(&self.pane.view, &self.ui, &self.window, &mut fresh)?; tree.update_and_process_changes(page, &mut Unwatched); } if focus != graft || self.ui.laid_out(page()).is_some() { @@ -4638,7 +4697,7 @@ impl State { if request.action == Action::Focus { self.window.focus_window(); self.ui.set_focus(Some(page())); - let response = self.view.focus_read_only(index)?; + let response = self.pane.view.focus_read_only(index)?; self.respond(response); self.window.request_redraw(); } @@ -4647,7 +4706,7 @@ impl State { let Some(outline) = self.accessibility.outline_for_node(request.target_node) else { return Ok(()); }; - let view = &mut self.view; + let view = &mut self.pane.view; match (request.action, request.data) { (Action::Focus, _) => { view.editor.focus_outline(outline)?; @@ -4671,7 +4730,7 @@ impl State { _ => return Ok(()), } self.ui.set_focus(Some(page())); - let response = self.view.focus_text()?; + let response = self.pane.view.focus_text()?; self.respond(response); self.window.request_redraw(); Ok(()) @@ -4682,8 +4741,12 @@ impl State { if let Some(path) = self.snapshot.take() { // A hidden window never paints, so nothing polls the pictures between snapshots. let paper = self.paper(); - if let Some((scene, _)) = &mut self.view.scene { - scene.settle(Some(&self.view.editor), self.view.viewport.scale, paper); + if let Some((scene, _)) = &mut self.pane.view.scene { + scene.settle( + Some(&self.pane.view.editor), + self.pane.view.viewport.scale, + paper, + ); } self.snapshot(&path)?; } @@ -4724,7 +4787,7 @@ impl State { /// Marks misspelled words on the page unless Hide Spelling Errors is on. fn show_spelling(&mut self) { - self.view.spelling = self.spelling.clone().filter(|_| !self.hide_spelling); + self.pane.view.spelling = self.spelling.clone().filter(|_| !self.hide_spelling); self.window.request_redraw(); } @@ -4736,7 +4799,7 @@ impl State { } .colored( self.page_color_preview - .unwrap_or_else(|| self.view.editor.page_color()), + .unwrap_or_else(|| self.pane.view.editor.page_color()), ) } @@ -4758,9 +4821,9 @@ impl State { fn paint(&mut self, target: &draw::Target) -> Result<(), Box> { let start = Instant::now(); let paper = self.paper(); - self.view.update_pictures(paper, &self.redraw); + self.pane.view.update_pictures(paper, &self.redraw); if let Some(session) = &self.session { - self.view.tag_art = session.library.tag_art(); + self.pane.view.tag_art = session.library.tag_art(); } let theme = &self.ui.theme; let scale = self.ui.scale(); @@ -4772,20 +4835,20 @@ impl State { canvas::gpu::Viewport { scale, origin: [0.0; 2], - ..self.view.viewport + ..self.pane.view.viewport }, ), None => ( - self.view.primitives(TextColors { + self.pane.view.primitives(TextColors { caret: theme.caret, - selection: if self.page_focused { + selection: if self.pane.page_focused { theme.selection } else { theme.inactive_selection }, paper, })?, - self.view.viewport, + self.pane.view.viewport, ), }; lap("page primitives", start); @@ -4853,14 +4916,17 @@ impl State { fn pinch(&mut self, factor: f32) -> Result<(), Box> { if self.over_page() { // The pointer here, as the page's own lags by the moves queued for the next frame. - let response = self.view.pinch(factor, self.page_point(self.pointer))?; + let response = self + .pane + .view + .pinch(factor, self.page_point(self.pointer))?; self.respond(response); } Ok(()) } fn swipe_scroll(&mut self, phase: winit::event::TouchPhase, delta: [f32; 2]) { - let scroll = self.view.scroll(); + let scroll = self.pane.view.scroll(); let armed = scroll.max[0] <= scroll.min[0] && self.over_page(); if let Some(forward) = self.swipe.scroll(phase, delta, armed, Instant::now()) { self.travel(forward); @@ -5209,6 +5275,7 @@ fn page_rows( theme: &Theme, section: &ui::Section, session: &Session, + showing: &Showing, marked: [&HashSet; 2], peers: &HashMap>, shape: (f32, bool), @@ -5255,6 +5322,7 @@ fn page_rows( theme, section, session, + showing, marked, peers, shape, @@ -5283,6 +5351,7 @@ fn page_rows( theme, section, session, + showing, marked, peers, shape, @@ -5305,6 +5374,7 @@ fn page_row( theme: &Theme, section: &ui::Section, session: &Session, + showing: &Showing, [found, unread]: [&HashSet; 2], peers: &HashMap>, shape: (f32, bool), @@ -5334,7 +5404,7 @@ fn page_row( shift, lifted, }; - let selected = *space == session.space && session.version.is_none(); + let selected = *space == showing.space && showing.version.is_none(); if selected && !lifted { rows.open = Some(ui.id(space)); } @@ -5366,19 +5436,19 @@ fn page_row( }; // Each row: its id, its label, whether it is open, and whether it is a page version. let history = (session.page_versions(*space).iter()) - .filter(|_| session.shown_history == Some(*space)) + .filter(|_| showing.shown_history == Some(*space)) .map(|version| { - let open = session.version == Some(version.context); + let open = showing.version == Some(version.context); (version.context, history::label(version), open, true) }); let conflicts = (versions.iter()) - .filter(|_| session.shown == Some(*space)) + .filter(|_| showing.shown == Some(*space)) .map(|version| { let label = match version.created { Some(created) => format!("{} {}", platform::short_date(created), version.user), None => version.user.clone(), }; - (version.space, label, version.space == session.space, false) + (version.space, label, version.space == showing.space, false) }); for (id, label, selected, version) in history.chain(conflicts) { let tab = PageTab { @@ -6236,14 +6306,14 @@ pub fn plain(error: &(dyn Error + 'static), thing: &str) -> String { } } -/// The session for `section`, at catalog `path` in `library`, showing `space` or its first -/// page, and that page. +/// The session for `section`, at catalog `path` in `library`, with `space` or its first +/// page and that page. fn read_session( section: notebook::session::Section, library: Arc, path: String, space: Option, -) -> Result<(Session, Page), Box> { +) -> Result<(Session, ExGuid, Page), Box> { let folder = path.rsplit_once('/').map_or("", |(folder, _)| folder); let tabs = library.tabs(folder); let tab = tabs @@ -6266,15 +6336,11 @@ fn read_session( tabs, tab, pages, - space, sync, conflicts, - shown: None, - change: None, history, - shown_history: None, - version: None, }, + space, page, )) } @@ -6371,7 +6437,7 @@ impl State { } UserEvent::InsertText(text) => { if self.ui.focused() == Some(page()) { - match self.view.insert_text(text) { + match self.pane.view.insert_text(text) { Ok(response) => self.respond(response), Err(error) => eprintln!("{error}"), } @@ -6488,7 +6554,7 @@ impl State { // Quiet still when the mark sent on finding it quiet arrives, what a thread sent // as it ended has been taken, however many frames wait in the queue before it. let quiet = self.settled() - && self.opening.is_none() + && self.pane.opening.is_none() && RUNNING.load(Ordering::Acquire) == 0 && self.search.pending.load(Ordering::Relaxed) == 0; if let Some((path, settled, sent)) = self.replay_settle.take() { @@ -6522,7 +6588,7 @@ impl State { if event.window_id != self.window.id() { return; } - let was_marked = self.view.editor.marked_range().is_some(); + let was_marked = self.pane.view.editor.marked_range().is_some(); let result = match event.window_event { accesskit_winit::WindowEvent::InitialTreeRequested => { self.deactivate_accessibility(); @@ -6534,7 +6600,7 @@ impl State { Ok(()) } }; - if was_marked && self.view.editor.marked_range().is_none() { + if was_marked && self.pane.view.editor.marked_range().is_none() { platform::clear_marked_text(&self.window); } if let Err(error) = result { @@ -6547,7 +6613,7 @@ impl State { impl State { /// Quits, first asking whether to discard a temporary page holding edits. fn quit(&self) { - let editor = &self.view.editor; + let editor = &self.pane.view.editor; let unchanged = editor.caret_outline().is_none_or(TextOutline::is_empty) && self.initial_date == editor.date().map(|date| date.timestamp()) && (self.initial_layouts.iter()) @@ -6804,7 +6870,7 @@ impl ApplicationHandler for App { WindowEvent::PinchGesture { delta, .. } => state.pinch(1.0 + delta as f32)?, // Smart zoom: to 200% about the fingers, or back to 100%. WindowEvent::DoubleTapGesture { .. } => { - let zoom = state.view.zoom(); + let zoom = state.pane.view.zoom(); let target = if (zoom - 1.0).abs() < 0.01 { 2.0 } else { 1.0 }; state.pinch(target / zoom)? } @@ -6826,7 +6892,7 @@ impl ApplicationHandler for App { return; } let now = Instant::now(); - let (repaint, blink) = state.view.blink(now); + let (repaint, blink) = state.pane.view.blink(now); // A due interface change waits on the frame that shows it, which sets the next. let wake = state.ui.wake_at(); if repaint || wake.is_some_and(|wake| wake <= now) { @@ -7562,7 +7628,7 @@ mod tests { let section = library.open(&tab.path, || {}).unwrap(); let opened = ms(start); let start = Instant::now(); - let (session, page) = + let (session, _, page) = read_session(section, Arc::clone(&library), tab.path.clone(), None).unwrap(); let read = ms(start); let start = Instant::now(); diff --git a/crates/snowbound/src/manage.rs b/crates/snowbound/src/manage.rs index c05697f6790fb64f2292d8dd80300ca273c87b30..d9590fa96d38c22021cea1a44d43b96316644cca 100644 --- a/crates/snowbound/src/manage.rs +++ b/crates/snowbound/src/manage.rs @@ -177,8 +177,8 @@ impl State { title: &str, ) -> Result> { let creation = PageCreation::new(before, Some(title), &self.author)?.titled_in( - &self.view.editor.default_font.face, - self.view.editor.default_font.color, + &self.pane.view.editor.default_font.face, + self.pane.view.editor.default_font.color, )?; let [date, time] = platform::date_text(creation.created()); Ok(creation.dated(&date, &time)?) @@ -228,7 +228,7 @@ impl State { session.section.replica().apply(&self.author, edit)?; #[cfg(target_arch = "wasm32")] session.section.apply(&self.author, edit)?; - let shown = session.space; + let shown = self.pane.showing.space; self.made(Change::Delete { pages: vec![space], show: Some(shown), @@ -236,7 +236,8 @@ impl State { }); let session = self.session.as_ref().ok_or("No section is open")?; let page = session.section.page(space)?; - let (scene, editor) = canvas::gpu::page::PageScene::from_page(page, &mut self.view.engine)?; + let (scene, editor) = + canvas::gpu::page::PageScene::from_page(page, &mut self.pane.view.engine)?; self.stop_loading(); let now = crate::Instant::now(); self.show_page(crate::Opening { @@ -248,14 +249,15 @@ impl State { })?; self.ui.set_focus(Some(crate::page())); if let Some(title) = self + .pane .view .editor .outlines() .iter() .find(|outline| outline.title) { - self.view.editor.focus_outline(title.id)?; - let response = self.view.focus_text()?; + self.pane.view.editor.focus_outline(title.id)?; + let response = self.pane.view.focus_text()?; self.respond(response); } Ok(()) @@ -267,7 +269,7 @@ impl State { self.with_art(Some(template), move |state, art| { state.persist()?; let session = state.session.as_ref().ok_or("No section is open")?; - let ops = template_ops(&session.section.page(session.space)?, name, art)?; + let ops = template_ops(&session.section.page(state.pane.showing.space)?, name, art)?; state.edit_page(ops) }) } @@ -283,16 +285,13 @@ impl State { let Some(template) = template else { return then(self, Vec::new()); }; - let space = self.session.as_ref().ok_or("No section is open")?.space; + self.session.as_ref().ok_or("No section is open")?; + let space = self.pane.showing.space; let proxy = self.proxy.clone(); crate::spawn(move || { let art = template.pictures().map_err(|error| error.to_string()); let _ = proxy.send_event(crate::UserEvent::Then(Box::new(move |state| { - if state - .session - .as_ref() - .is_some_and(|open| open.space == space) - { + if state.session.is_some() && state.pane.showing.space == space { then(state, art?) } else { Ok(()) @@ -310,7 +309,7 @@ impl State { rule_lines: Option, art: Option>, ) -> Result<(), Box> { - let response = self.view.set_paper(color, rule_lines, art)?; + let response = self.pane.view.set_paper(color, rule_lines, art)?; self.respond(response); Ok(()) } @@ -321,7 +320,7 @@ impl State { return Ok(()); } let session = self.session.as_ref().ok_or("No section is open")?; - let space = session.space; + let space = self.pane.showing.space; session.section.apply( &self.author, Edit { @@ -469,6 +468,7 @@ impl State { pub(crate) fn show_arrived(&mut self, library: &Arc) { if self.session.is_some() || self + .pane .sectionless .as_ref() .is_some_and(|shown| shown.location != library.location) @@ -476,7 +476,7 @@ impl State { return; } let first = library.first_section(); - self.sectionless = Some(Arc::clone(library)); + self.pane.sectionless = Some(Arc::clone(library)); self.title(); if let Some(path) = first { self.commands @@ -572,8 +572,8 @@ impl State { .first_section() .ok_or("The new notebook has no section")?; let section = library.open_async(&path, notify).await?; - let (session, page) = read_session(section, library, path, None)?; - Ok(Loaded::Section(Box::new(session), page)) + let (session, space, page) = read_session(section, library, path, None)?; + Ok(Loaded::Section(Box::new(session), space, page)) }); Ok(()) } @@ -623,7 +623,7 @@ impl State { background.discard(); } let shown = |shown: &Arc| shown.location == library.location; - if self.sectionless.as_ref().is_some_and(shown) + if self.pane.sectionless.as_ref().is_some_and(shown) || self .session .as_ref() @@ -631,7 +631,7 @@ impl State { { self.persist().unwrap_or_else(|error| eprintln!("{error}")); self.session = None; - self.sectionless = None; + self.pane.sectionless = None; match self .notebooks .iter() @@ -639,7 +639,7 @@ impl State { { Some((open, path)) => self.commands.push(Command::OpenSection(open, path)), None => { - self.sectionless = self.notebooks.first().cloned(); + self.pane.sectionless = self.notebooks.first().cloned(); self.title(); } } @@ -695,15 +695,16 @@ impl State { (self.session).take_if(|session| Arc::ptr_eq(&session.library, &library)) { // Its replica goes with it, so nothing may hold it. - let _ = self.view.editor.take_ops(); + let _ = self.pane.view.editor.take_ops(); if let Err(error) = session.section.close() { eprintln!("{location}: {error}"); } - self.sectionless = Some(Arc::clone(&library)); + self.pane.sectionless = Some(Arc::clone(&library)); } self.trashing.insert(location.clone()); self.close_notebook(&library); - self.trail.forget(&location); + self.pane.trail.forget(&location); + self.recent.retain(|place| place.notebook != location); self.reads.forget(&location); let ours = crate::library::key(&location, ""); self.folded.retain(|key| !key.starts_with(&ours)); @@ -754,13 +755,13 @@ impl State { .as_ref() .and_then(|_| self.session.take()) .map(|session| session.section); - let showing = shown.is_some() || self.sectionless.as_ref().is_some_and(ours); + let showing = shown.is_some() || self.pane.sectionless.as_ref().is_some_and(ours); if showing { - self.sectionless = Some(Arc::clone(&library)); + self.pane.sectionless = Some(Arc::clone(&library)); self.title(); } self.folder_rename = Some((library.location.clone(), to.clone())); - let (cache, proxy, requested) = (self.cache.clone(), self.proxy.clone(), self.loading); + let (cache, proxy, requested) = (self.cache.clone(), self.proxy.clone(), self.pane.loading); crate::spawn_async(move || async move { let structure = Arc::clone(&library.structure); let _structure = structure.lock().await; @@ -805,7 +806,7 @@ impl State { state.folder_rename = None; } let restore = showing - && requested == state.loading + && requested == state.pane.loading && state.notebook().is_some_and(|current| { current.location == library.location && Arc::ptr_eq(¤t.structure, &library.structure) @@ -839,7 +840,14 @@ impl State { .retain(|open| open.location != to || !std::mem::replace(&mut listed, true)); if from != to { self.undo.close(&from); - self.trail.moved(&from, &to); + self.pane.trail.moved(&from, &to); + for place in self + .recent + .iter_mut() + .filter(|place| place.notebook == from) + { + place.notebook.clone_from(&to); + } self.reads.moved(&from, &to); let (from, to) = (crate::library::key(&from, ""), crate::library::key(&to, "")); let rekey = |key: &String| match key.strip_prefix(&from) { @@ -852,7 +860,7 @@ impl State { .collect(); } if let Some(shown) = shown { - self.sectionless = Some(Arc::clone(&reopened)); + self.pane.sectionless = Some(Arc::clone(&reopened)); self.title(); if let Some(path) = shown .filter(|path| reopened.contains(path)) @@ -885,8 +893,8 @@ impl State { .filter(|session| Arc::ptr_eq(&session.library, &library)) .map(|session| session.tabs[session.tab].path.clone()); let notify = notify(self.proxy.clone()); - let (proxy, requested) = (self.proxy.clone(), self.loading); - let showing = self.switching.is_none() && self.opening.is_none(); + let (proxy, requested) = (self.proxy.clone(), self.pane.loading); + let showing = self.pane.switching.is_none() && self.pane.opening.is_none(); let structure = Arc::clone(&library.structure); let pending = match &change { Structure::Rename { path, name } => library.entry_identity(path).map(|identity| { @@ -991,8 +999,8 @@ impl State { library.keep_key(&path, key); } let section = library.open_async(&path, notify).await?; - let (session, page) = read_session(section, library, path, None)?; - return Ok(Loaded::Section(Box::new(session), page)); + let (session, space, page) = read_session(section, library, path, None)?; + return Ok(Loaded::Section(Box::new(session), space, page)); } Structure::Properties { name, color } => { if name != library.name { @@ -1018,11 +1026,11 @@ impl State { return Ok(Loaded::Library(library, Some(path))); } let section = library.open_async(&path, notify).await?; - let (session, page) = read_session(section, library, path, None)?; + let (session, space, page) = read_session(section, library, path, None)?; Ok(if fresh { - Loaded::Created(Box::new(session), page) + Loaded::Created(Box::new(session), space, page) } else { - Loaded::Section(Box::new(session), page) + Loaded::Section(Box::new(session), space, page) }) }; crate::spawn_async(move || async move { @@ -1040,7 +1048,7 @@ impl State { }; let library = match &loaded { Loaded::Library(library, _) => Arc::clone(library), - Loaded::Section(session, _) | Loaded::Created(session, _) => { + Loaded::Section(session, ..) | Loaded::Created(session, ..) => { Arc::clone(&session.library) } _ => unreachable!("A structure change returns its notebook"), @@ -1071,7 +1079,7 @@ impl State { }); if let Some(path) = path { state.adopt(library, Some(&path))?; - } else if requested != state.loading || !showing { + } else if requested != state.pane.loading || !showing { if let Some(path) = library.first_section() { state.commands.push(Command::OpenSection(library, path)); } else { @@ -1079,13 +1087,14 @@ impl State { } } } else if state + .pane .sectionless .as_ref() .is_some_and(|shown| shown.location == library.location) { - state.sectionless = Some(library); + state.pane.sectionless = Some(library); } - if showing && requested == state.loading { + if showing && requested == state.pane.loading { match loaded { Loaded::Library(library, path) => state.adopt(library, path.as_deref())?, loaded => state.load(move || Ok(loaded)), diff --git a/crates/snowbound/src/menus.rs b/crates/snowbound/src/menus.rs index ee15575d8abafad1e07a01e50bf542b2e7805f21..8a53b18495d85ca4801c5da94991dd6d4d7f748f 100644 --- a/crates/snowbound/src/menus.rs +++ b/crates/snowbound/src/menus.rs @@ -489,7 +489,7 @@ impl State { let above = at .and_then(|at| at.checked_sub(1)) .map_or(0, |above| pages[above].2); - let shown = session.shown_history == Some(*space); + let shown = self.pane.showing.shown_history == Some(*space); let versions = if shown { "Hide Page Versions" } else { @@ -1039,7 +1039,7 @@ impl State { new(&action, String::new()).map(|structure| Command::Structure(library, structure)) } (Target::Closed(location), Action::Delete) => { - self.trail.recent.retain(|place| place.notebook != location); + self.recent.retain(|place| place.notebook != location); self.save_settings(); None } diff --git a/crates/snowbound/src/navigation.rs b/crates/snowbound/src/navigation.rs index 8529053ea18912febcf5471285662c326dc67919..c9b9d113f77bee7ec48255d8b12891bbe644e776 100644 --- a/crates/snowbound/src/navigation.rs +++ b/crates/snowbound/src/navigation.rs @@ -39,19 +39,17 @@ impl Visit { } } -/// Pages visited before and after the one shown, and those shown lately. +/// Pages visited before and after the one shown. #[derive(Default)] pub struct Trail { back: Vec, here: Option, forward: Vec, - /// Each page shown lately once, latest first, kept between launches. - pub recent: Vec, } /// Places Back keeps, at most. const KEPT: usize = 100; -/// Pages `Trail::recent` keeps, at most. +/// Pages `State::recent` keeps, at most. const RECENT: usize = 8; impl Trail { @@ -61,8 +59,7 @@ impl Trail { .chain(&mut self.here) .chain(&mut self.forward) .map(|visit| &mut visit.notebook); - let places = self.recent.iter_mut().map(|place| &mut place.notebook); - for notebook in visits.chain(places).filter(|notebook| *notebook == from) { + for notebook in visits.filter(|notebook| *notebook == from) { *notebook = to.to_owned(); } } @@ -73,14 +70,6 @@ impl Trail { self.back.retain(elsewhere); self.forward.retain(elsewhere); self.here = self.here.take().filter(elsewhere); - self.recent.retain(|place| place.notebook != location); - } - - /// Notes `place` shown, as the recent pages list it. - pub fn remember(&mut self, place: Place) { - self.recent.retain(|recent| *recent != place); - self.recent.insert(0, place); - self.recent.truncate(RECENT); } /// Notes `visit` shown. Arriving anywhere but where Back or Forward went drops the pages @@ -122,23 +111,30 @@ impl Trail { pub fn open(&self, exists: impl Fn(&Visit) -> bool) -> [bool; 2] { [&self.back, &self.forward].map(|side| side.iter().rev().any(&exists)) } +} - /// Drops the recent pages whose section `listed` refuses, and those whose section `index` - /// has read without them. Returns whether any went. - pub fn prune(&mut self, index: &Index, listed: impl Fn(&Place) -> bool) -> bool { - let read: HashSet<&str> = (index.entries().iter()) - .map(|entry| entry.section.as_str()) - .collect(); - let before = self.recent.len(); - self.recent.retain(|place| { - let key = crate::library::key(&place.notebook, &place.section); - listed(place) - && (!read.contains(key.as_str()) - || (index.entries().iter()) - .any(|entry| entry.space == place.page && entry.section == key)) - }); - self.recent.len() != before - } +/// Notes `place` shown, as the recent pages list it. +pub fn remember(recent: &mut Vec, place: Place) { + recent.retain(|known| *known != place); + recent.insert(0, place); + recent.truncate(RECENT); +} + +/// Drops the recent pages whose section `listed` refuses, and those whose section `index` +/// has read without them. Returns whether any went. +pub fn prune(recent: &mut Vec, index: &Index, listed: impl Fn(&Place) -> bool) -> bool { + let read: HashSet<&str> = (index.entries().iter()) + .map(|entry| entry.section.as_str()) + .collect(); + let before = recent.len(); + recent.retain(|place| { + let key = crate::library::key(&place.notebook, &place.section); + listed(place) + && (!read.contains(key.as_str()) + || (index.entries().iter()) + .any(|entry| entry.space == place.page && entry.section == key)) + }); + recent.len() != before } /// Where `visit`'s page is in `notebooks`, as `index` has read them and `open` tells whether @@ -189,19 +185,19 @@ impl State { let place = Place { notebook: session.library.location.clone(), section: path.clone(), - page: session.space, + page: self.pane.showing.space, }; if let Ok(section) = session.section.identity() { - self.trail.visit(Visit { + self.pane.trail.visit(Visit { notebook: place.notebook.clone(), section, - space: session.space, - page: self.view.editor.identity(), + space: self.pane.showing.space, + page: self.pane.view.editor.identity(), binned: crate::recycle::binned(path), }); } - let moved = self.trail.recent.first() != Some(&place); - self.trail.remember(place); + let moved = self.recent.first() != Some(&place); + remember(&mut self.recent, place); if moved { self.save_settings(); } @@ -222,7 +218,7 @@ impl State { /// Whether Back and Forward have a page to go to. pub(crate) fn can_travel(&self) -> [bool; 2] { - self.trail.open(|visit| self.find(visit).is_some()) + self.pane.trail.open(|visit| self.find(visit).is_some()) } /// Whether `library`'s section at `path` is the one open. @@ -246,9 +242,9 @@ impl State { /// Shows the page visited before the one shown, or after it going `forward`, wherever it /// went since. Pages since deleted, and those of notebooks since closed, are passed over. pub(crate) fn travel(&mut self, forward: bool) { - let mut trail = std::mem::take(&mut self.trail); + let mut trail = std::mem::take(&mut self.pane.trail); let visit = trail.step(forward, |visit| self.find(visit).is_some()); - self.trail = trail; + self.pane.trail = trail; if let Some((library, path, space)) = visit.and_then(|visit| self.find(&visit)) { self.go(library, path, space); } @@ -418,16 +414,16 @@ mod tests { #[test] fn recent_pages_are_kept_once_latest_first_and_bounded() { - let mut trail = Trail::default(); + let mut recent = Vec::new(); for page in [1, 2, 1] { - trail.remember(place("A.one", page)); + remember(&mut recent, place("A.one", page)); } - assert_eq!(trail.recent, [place("A.one", 1), place("A.one", 2)]); + assert_eq!(recent, [place("A.one", 1), place("A.one", 2)]); for page in 0..20 { - trail.remember(place("B.one", page)); + remember(&mut recent, place("B.one", page)); } - assert_eq!(trail.recent.len(), RECENT); - assert_eq!(trail.recent[0], place("B.one", 19)); + assert_eq!(recent.len(), RECENT); + assert_eq!(recent[0], place("B.one", 19)); } #[test] @@ -452,7 +448,7 @@ mod tests { &page("Kept"), 0, )); - let mut trail = Trail::default(); + let mut recent = Vec::new(); // Visited oldest first: the index has read A without page 2, not yet B, and C is gone. for gone in [ place("C.one", 1), @@ -460,12 +456,12 @@ mod tests { place("A.one", 2), kept.clone(), ] { - trail.remember(gone); + remember(&mut recent, gone); } let listed = |place: &Place| place.section != "C.one"; - assert!(trail.prune(&index, listed)); - assert_eq!(trail.recent, [kept, place("B.one", 1)]); - assert!(!trail.prune(&index, listed), "nothing more goes"); + assert!(prune(&mut recent, &index, listed)); + assert_eq!(recent, [kept, place("B.one", 1)]); + assert!(!prune(&mut recent, &index, listed), "nothing more goes"); } #[test] diff --git a/crates/snowbound/src/options.rs b/crates/snowbound/src/options.rs index 7ce4c327ca01d80b49b13694815ddbad4d76c7f8..ddcb6e3ec4b9cad86b4d28818f719d74f0ddb0fa 100644 --- a/crates/snowbound/src/options.rs +++ b/crates/snowbound/src/options.rs @@ -390,10 +390,10 @@ impl State { color_scheme: self.color_scheme, pages_match: !self.light_pages, hide_spelling: self.hide_spelling, - markdown: self.view.editor.markdown.is_some(), + markdown: self.pane.view.editor.markdown.is_some(), automatic_updates: self.updates.automatic(), pen_pressure: self.pen_pressure, - default_font: self.view.editor.default_font.clone(), + default_font: self.pane.view.editor.default_font.clone(), page_tabs_left: self.page_tabs_left, navigation_bar_left: !self.navigation_bar_right, lock_after: self.passwords.lock_after, @@ -692,9 +692,9 @@ impl State { self.color_scheme = options.color_scheme; self.light_pages = !options.pages_match; self.hide_spelling = options.hide_spelling; - self.view.editor.markdown = crate::commands::markdown(options.markdown); + self.pane.view.editor.markdown = crate::commands::markdown(options.markdown); self.pen_pressure = options.pen_pressure; - self.view.editor.default_font = options.default_font; + self.pane.view.editor.default_font = options.default_font; self.page_tabs_left = options.page_tabs_left; self.navigation_bar_right = !options.navigation_bar_left; self.passwords = crate::settings::Passwords { diff --git a/crates/snowbound/src/palette.rs b/crates/snowbound/src/palette.rs index 39778bf2636f3a90467f6bfde196da841dbeaced..cf154e4fed7ad5e88fd6c1cee75269e6a516f50d 100644 --- a/crates/snowbound/src/palette.rs +++ b/crates/snowbound/src/palette.rs @@ -114,7 +114,7 @@ impl State { } } // And what the page's context menu does at the caret, or to the file selected. - let text = self.view.caret_context(); + let text = self.pane.view.caret_context(); for (item, title) in text.iter().flat_map(|context| self.text_items(context)) { if !(commands::COMMANDS.iter()).any(|command| command.title == title) { commands.push(Row { @@ -334,7 +334,7 @@ impl State { Target::Page { library: Arc::clone(library), path: path.clone(), - space: session.space, + space: self.pane.showing.space, }, Target::Section { library: Arc::clone(library), @@ -367,7 +367,7 @@ impl State { .map(Arc::clone) }; // A closed notebook keeps its places to open it again, an unreadable one until it reads. - let pruned = self.trail.prune(&index, |place| { + let pruned = crate::navigation::prune(&mut self.recent, &index, |place| { library(&place.notebook) .is_none_or(|library| library.notebook.is_err() || library.contains(&place.section)) }); @@ -375,7 +375,7 @@ impl State { ( session.library.location.as_str(), session.tabs[session.tab].path.as_str(), - session.space, + self.pane.showing.space, ) }); let theme = &self.ui.theme; @@ -393,7 +393,7 @@ impl State { _ => false, }) }; - for place in &self.trail.recent { + for place in &self.recent { let Some(library) = library(&place.notebook) else { let target = Target::Closed(place.notebook.clone()); if !listed(&closed, &target) { diff --git a/crates/snowbound/src/paste.rs b/crates/snowbound/src/paste.rs index 9466635cd383ea0011050630e6088891a0438bd7..5f805c92128658aa431b7ead4415745a688770c6 100644 --- a/crates/snowbound/src/paste.rs +++ b/crates/snowbound/src/paste.rs @@ -124,7 +124,7 @@ impl State { } } Some(Pasted::Clip(clip)) => { - let response = self.view.paste_clip(clip)?; + let response = self.pane.view.paste_clip(clip)?; self.respond(response); } Some(Pasted::Page(html, text)) => { @@ -142,12 +142,12 @@ impl State { }); if pieces.is_empty() { if let Some(text) = text { - let response = self.view.paste(&text, language)?; + let response = self.pane.view.paste(&text, language)?; self.respond(response); } return Ok(()); } - let (places, response) = self.view.paste_pieces(pieces)?; + let (places, response) = self.pane.view.paste_pieces(pieces)?; self.respond(response); for (place, (source, css)) in places.into_iter().zip(fetched) { let proxy = self.proxy.clone(); @@ -159,7 +159,7 @@ impl State { return; }; let _ = proxy.send_event(crate::UserEvent::Then(Box::new(move |state| { - let response = state.view.insert_awaited(place, bytes, size)?; + let response = state.pane.view.insert_awaited(place, bytes, size)?; state.respond(response); Ok(()) }))); @@ -168,7 +168,7 @@ impl State { } Some(Pasted::Text(text)) => { let language = canvas::language::lcid(&platform::input_language()); - let response = self.view.paste(&text, language)?; + let response = self.pane.view.paste(&text, language)?; self.respond(response); } Some(Pasted::Picture(bytes)) => self.insert_picture(bytes, None)?, @@ -194,9 +194,10 @@ impl State { let size = picture_size(&bytes, pixels); let response = match at { Some(point) => self + .pane .view .drop_picture(self.page_point(point), bytes, size)?, - None => self.view.insert_picture(bytes, size)?, + None => self.pane.view.insert_picture(bytes, size)?, }; self.respond(response); Ok(()) diff --git a/crates/snowbound/src/prefetch.rs b/crates/snowbound/src/prefetch.rs index 5e450671a70c2e5d345f59d593b4566891cd95cf..377d26fb8bf243be1055cc7ded6bf3d91e8655be 100644 --- a/crates/snowbound/src/prefetch.rs +++ b/crates/snowbound/src/prefetch.rs @@ -277,7 +277,7 @@ impl State { /// The section tab shown open: one being opened, from the moment it is asked for. pub fn open_tab(&self) -> Option { let session = self.session.as_ref()?; - let opening = self.switching.and_then(|(tab, _)| tab); + let opening = self.pane.switching.and_then(|(tab, _)| tab); Some( opening .filter(|tab| *tab < session.tabs.len()) @@ -287,15 +287,15 @@ impl State { /// Since when the page area shows that a page is opening, once it has taken a while. pub fn loading(&self) -> Option { - let (_, since) = self.switching?; + let (_, since) = self.pane.switching?; (since.elapsed() >= PATIENCE).then_some(since) } /// Leaves the page shown as it is, dropping whatever was opening in its place. pub fn stop_loading(&mut self) { - self.loading += 1; - self.opening = None; - self.switching = None; + self.pane.loading += 1; + self.pane.opening = None; + self.pane.switching = None; } /// Readies the pages beside the open one and the sections beside the open section. @@ -306,7 +306,7 @@ impl State { let at = session .pages .iter() - .position(|(space, ..)| *space == session.space); + .position(|(space, ..)| *space == self.pane.showing.space); let pages: Vec = at .into_iter() .flat_map(|at| [at + 1, at.wrapping_sub(1)]) @@ -330,7 +330,7 @@ impl State { return; }; let key = session.key(); - if space == session.space || !self.prefetch.ask((key.clone(), Some(space))) { + if space == self.pane.showing.space || !self.prefetch.ask((key.clone(), Some(space))) { return; } let read = session.reader(space); diff --git a/crates/snowbound/src/print.rs b/crates/snowbound/src/print.rs index 87c3fb0ccd96b160023543eeb8e71cfc9e07defc..23c0c6e19e39185289ea33843242be9f12893825 100644 --- a/crates/snowbound/src/print.rs +++ b/crates/snowbound/src/print.rs @@ -281,11 +281,11 @@ impl State { let session = self.session.as_ref().ok_or("No section is open")?; let tab = &session.tabs[session.tab]; // The open page as the editor holds it; the others as stored. - let open = (session.space, self.view.editor.page()?); + let open = (self.pane.showing.space, self.pane.view.editor.page()?); let at = session .pages .iter() - .position(|(space, ..)| *space == session.space); + .position(|(space, ..)| *space == self.pane.showing.space); let spaces: Vec = match (setup.scope, at) { (Scope::PageGroup, Some(at)) => { let levels: Vec = session.pages.iter().map(|(.., level)| *level).collect(); @@ -297,7 +297,7 @@ impl State { (Scope::Section | Scope::Notebook, _) => { session.pages.iter().map(|(space, ..)| *space).collect() } - _ => vec![session.space], + _ => vec![self.pane.showing.space], }; let title = match setup.scope { Scope::Page | Scope::PageGroup => open.1.title.clone(), diff --git a/crates/snowbound/src/protection.rs b/crates/snowbound/src/protection.rs index 87c93cf06142b838243b8dfd5da7e1b78ff651a6..11cd31eba964a919c93a42e5b4fd72ebc1131ea6 100644 --- a/crates/snowbound/src/protection.rs +++ b/crates/snowbound/src/protection.rs @@ -89,8 +89,8 @@ impl State { previous.library.keep(path, previous.section); } } - self.sectionless = None; - self.locked = Some(Locked { library, path }); + self.pane.sectionless = None; + self.pane.locked = Some(Locked { library, path }); self.ui.set_focus(Some(notice())); self.title(); Ok(()) @@ -113,7 +113,7 @@ impl State { section: &ui::Section, strip: [f32; 4], ) -> (Option, Id) { - let Some(locked) = &self.locked else { + let Some(locked) = &self.pane.locked else { return (None, row); }; let library = Arc::clone(&locked.library); @@ -157,7 +157,7 @@ impl State { /// The locked section's page, as OneNote 2010 draws it: a click or Enter unlocks. pub(crate) fn locked_page(&mut self) { - let Some(locked) = &self.locked else { + let Some(locked) = &self.pane.locked else { return; }; let (library, path) = (Arc::clone(&locked.library), locked.path.clone()); @@ -266,7 +266,7 @@ impl State { /// The section shown, open or locked, with its notebook. pub(crate) fn shown_section(&self) -> Option<(Arc, String)> { - match (&self.session, &self.locked) { + match (&self.session, &self.pane.locked) { (Some(session), _) if session.library.catalog().is_some() => Some(( Arc::clone(&session.library), session.tabs[session.tab].path.clone(), diff --git a/crates/snowbound/src/recording.rs b/crates/snowbound/src/recording.rs index b38b85cb076a0e0110a74c053339aa70837b260d..1482587e9b866eb08a891a14485b5b601883649d 100644 --- a/crates/snowbound/src/recording.rs +++ b/crates/snowbound/src/recording.rs @@ -93,7 +93,7 @@ impl State { }; let [date, time] = platform::date_text(crate::filetime()); let label = canvas::recording::label(video, &date, &time); - let (id, response) = self.view.start_recording(&label)?; + let (id, response) = self.pane.view.start_recording(&label)?; self.respond(response); if id.is_none() { recorder.stop()?()?; @@ -117,7 +117,7 @@ impl State { path, } => { // Nothing written while the file saves links to the recording. - self.view.editor.pause_recording(true); + self.pane.view.editor.pause_recording(true); let finish = match recorder.stop() { Ok(finish) => finish, Err(detail) => { @@ -126,8 +126,8 @@ impl State { } }; let recorded = path.clone(); - let id = self.view.editor.recording(); - let title = self.view.editor.page()?.title; + let id = self.pane.view.editor.recording(); + let title = self.pane.view.editor.page()?.title; let job = std::thread::spawn(move || { finish()?; let recorded = @@ -150,7 +150,7 @@ impl State { let saved = job .join() .unwrap_or_else(|_| Err("Try recording again.".into())); - if self.view.editor.recording().is_none() { + if self.pane.view.editor.recording().is_none() { return Ok(()); } let mut file = match saved { @@ -162,7 +162,7 @@ impl State { }; file.preview = platform::file_icon(&path).map(Into::into); notebook::fs::remove_file(&path)?; - let response = self.view.finish_recording(file)?; + let response = self.pane.view.finish_recording(file)?; self.respond(response); Ok(()) } @@ -225,7 +225,7 @@ impl State { let (enabled, checked) = match transport { Transport::Pause => ( recording || playing.is_some(), - Some(self.view.editor.recording_paused() || playing == Some(false)), + Some(self.pane.view.editor.recording_paused() || playing == Some(false)), ), Transport::Stop => (recording || playing.is_some(), None), Transport::Skip(_) | Transport::SeekTo => (playing.is_some(), None), @@ -241,9 +241,9 @@ impl State { (Transport::Stop, Media::Recording { .. }) => self.stop_recording(false)?, (Transport::Stop, Media::Playing(_)) => self.media = Media::Idle, (Transport::Pause, Media::Recording { recorder, .. }) => { - let paused = !self.view.editor.recording_paused(); + let paused = !self.pane.view.editor.recording_paused(); recorder.pause(paused); - self.view.editor.pause_recording(paused); + self.pane.view.editor.pause_recording(paused); } (Transport::Pause, Media::Playing(playback)) => { let player = &mut playback.player; @@ -281,13 +281,13 @@ impl State { let mut played = None; let (clock_text, picture) = match &mut self.media { Media::Idle => { - let response = self.view.set_played(None)?; + let response = self.pane.view.set_played(None)?; self.respond(response); return Ok(()); } Media::Recording { video, .. } => { - let at = self.view.editor.recording_ms().unwrap_or(0); - let state = if self.view.editor.recording_paused() { + let at = self.pane.view.editor.recording_ms().unwrap_or(0); + let state = if self.pane.view.editor.recording_paused() { "Paused" } else if *video { "Recording video" @@ -311,8 +311,8 @@ impl State { played = playback .recording .filter(|_| self.see_playback) - .and_then(|id| self.view.editor.played_note(id, at)) - .and_then(|note| canvas::search::paragraph_match(&self.view.editor, note)); + .and_then(|id| self.pane.view.editor.played_note(id, at)) + .and_then(|note| canvas::search::paragraph_match(&self.pane.view.editor, note)); let mut picture = None; if let Some(video) = &mut playback.video { let frame = video.movie.frame(at); @@ -344,7 +344,7 @@ impl State { ) } }; - let response = self.view.set_played(played)?; + let response = self.pane.view.set_played(played)?; self.respond(response); let picture_height = picture .as_ref() diff --git a/crates/snowbound/src/recycle.rs b/crates/snowbound/src/recycle.rs index 303d32b078226bddff33e7d3725f3f30120d2a76..9d0788f59e3c461ae2f8350913e524b8a6279cdd 100644 --- a/crates/snowbound/src/recycle.rs +++ b/crates/snowbound/src/recycle.rs @@ -67,8 +67,8 @@ impl State { section.close_async().await?; continue; } - let (session, page) = read_session(section, library, path, None)?; - return Ok(Loaded::Section(Box::new(session), page)); + let (session, space, page) = read_session(section, library, path, None)?; + return Ok(Loaded::Section(Box::new(session), space, page)); } Err("The Recycle Bin is empty.".into()) }); @@ -127,7 +127,7 @@ impl State { let session = self.session.as_ref().ok_or("No section is open")?; let replica = Arc::clone(session.section.replica()); let library = Arc::clone(&session.library); - let shown = session.space; + let shown = self.pane.showing.space; let at = (session.pages.iter()).position(|(space, ..)| gone.contains(space)); let kept = |space: &ExGuid| !gone.contains(space); let after = at.and_then(|at| { @@ -149,8 +149,8 @@ impl State { .first_section() .ok_or("The notebook has no sections")?; let section = library.open_async(&path, notify(proxy)).await?; - let (session, page) = read_session(section, library, path, None)?; - Ok(Loaded::Section(Box::new(session), page)) + let (session, space, page) = read_session(section, library, path, None)?; + Ok(Loaded::Section(Box::new(session), space, page)) }); Ok(()) } diff --git a/crates/snowbound/src/save_as.rs b/crates/snowbound/src/save_as.rs index 0ca6dd3922abe0887057a309284263085738e5f9..17a1b3776232b6256897a1d6ee5d244228cde154 100644 --- a/crates/snowbound/src/save_as.rs +++ b/crates/snowbound/src/save_as.rs @@ -166,11 +166,15 @@ impl State { .filter(|session| Arc::ptr_eq(&session.library, &library)) .map(|session| { let path = session.tabs[session.tab].path.clone(); - (path, Arc::clone(session.section.replica()), session.space) + ( + path, + Arc::clone(session.section.replica()), + self.pane.showing.space, + ) }); let section = dialog.section.unwrap_or_default(); let title = match dialog.scope { - Scope::Page => self.view.editor.page()?.title, + Scope::Page => self.pane.view.editor.page()?.title, Scope::Section => crate::library::section_name(§ion, &None), Scope::Notebook => library.name.clone(), }; diff --git a/crates/snowbound/src/screenshot.rs b/crates/snowbound/src/screenshot.rs index f9306c49919a939066b1d5ec619d94727063211c..40509225815d701931f89a666f0990db30c9bdcf 100644 --- a/crates/snowbound/src/screenshot.rs +++ b/crates/snowbound/src/screenshot.rs @@ -59,17 +59,18 @@ impl State { }; self.surface.size = size.map(|side| (side * scale).max(1.0) as u32); self.renderer.clear_glyph_cache(); - let response = self.view.scale_factor_changed(scale)?; + let response = self.pane.view.scale_factor_changed(scale)?; self.respond(response); self.ui.set_focus(None); if let Some(title) = self + .pane .view .editor .outlines() .iter() .find(|outline| outline.title) { - self.view.editor.focus_outline(title.id)?; + self.pane.view.editor.focus_outline(title.id)?; } #[cfg(target_os = "macos")] if snow_leopard() { @@ -99,8 +100,12 @@ impl State { // The page's box sizes the view whose pictures settle. self.layout(size, scale)?; let paper = self.paper(); - if let Some((scene, _)) = &mut self.view.scene { - scene.settle(Some(&self.view.editor), self.view.viewport.scale, paper); + if let Some((scene, _)) = &mut self.pane.view.scene { + scene.settle( + Some(&self.pane.view.editor), + self.pane.view.viewport.scale, + paper, + ); } // Later frames lay out with earlier frames' measurements while colours ease. let deadline = Instant::now() + std::time::Duration::from_secs(2); diff --git a/crates/snowbound/src/search.rs b/crates/snowbound/src/search.rs index 9a58c22be9792a5b92ba24c286107db17fe364b3..f979e9717b710ba77b4f1b92d7a5b5c06a6bf271 100644 --- a/crates/snowbound/src/search.rs +++ b/crates/snowbound/src/search.rs @@ -614,26 +614,26 @@ impl State { /// Marks the query's matches on the page; with `select`, selects the match Find on /// This Page is at, or the first, or the paragraph, when a result opened the page. pub(crate) fn refind(&mut self, select: bool) -> Result<(), Box> { - self.view.found = page_matches(&self.view.editor, &self.search.shown()); + self.pane.view.found = page_matches(&self.pane.view.editor, &self.search.shown()); if !select { return Ok(()); } - let space = self.session.as_ref().map(|session| session.space); + let space = self.session.as_ref().map(|_| self.pane.showing.space); let target = if self.search.finding { self.search.current = match self.search.current { - Some(at) if at < self.view.found.len() => Some(at), - _ => (!self.view.found.is_empty()).then_some(0), + Some(at) if at < self.pane.view.found.len() => Some(at), + _ => (!self.pane.view.found.is_empty()).then_some(0), }; self.search .current - .and_then(|at| self.view.found.get(at).copied()) + .and_then(|at| self.pane.view.found.get(at).copied()) } else if let Some((_, paragraph)) = self.search.reveal.filter(|(page, _)| Some(*page) == space) { self.search.reveal = None; match paragraph { - Some(paragraph) => paragraph_match(&self.view.editor, paragraph), - None => self.view.found.first().copied(), + Some(paragraph) => paragraph_match(&self.pane.view.editor, paragraph), + None => self.pane.view.found.first().copied(), } } else { None @@ -645,9 +645,9 @@ impl State { let Some((outline, selection)) = target else { return Ok(()); }; - self.view.editor.focus_outline(outline)?; - self.view.editor.select(selection)?; - let response = self.view.focus_text()?; + self.pane.view.editor.focus_outline(outline)?; + self.pane.view.editor.select(selection)?; + let response = self.pane.view.focus_text()?; self.respond(response); Ok(()) } @@ -686,7 +686,7 @@ impl State { if !matches!(self.search.task_pane, Some(TaskPane::Search { .. })) { self.search.query.clear(); self.search.scope = self.search.default; - self.view.found.clear(); + self.pane.view.found.clear(); } } @@ -711,7 +711,7 @@ impl State { let open = self .session .as_ref() - .map(|session| (session.key(), session.space)); + .map(|session| (session.key(), self.pane.showing.space)); match open { Some((key, shown)) if key == section && shown == space => self.refind(true)?, Some((key, _)) if key == section => self.commands.push(Command::OpenPage(space)), @@ -1130,7 +1130,7 @@ impl State { &[NamedKey::Enter, NamedKey::F3, NamedKey::Escape], ); let before = self.search.query.clone(); - let count = self.view.found.len(); + let count = self.pane.view.found.len(); let label = if self.search.query.trim().is_empty() { "Find on page".to_owned() } else if count == 0 { @@ -1211,7 +1211,7 @@ impl State { self.end_search(); return Ok(()); } - let shift = self.view.modifiers().shift; + let shift = self.pane.view.modifiers().shift; for key in keys { match key { NamedKey::Escape => { @@ -1234,7 +1234,7 @@ impl State { (at + count - 1) % count }; self.search.current = Some(next); - self.select(self.view.found.get(next).copied())?; + self.select(self.pane.view.found.get(next).copied())?; } Ok(()) } diff --git a/crates/snowbound/src/settings.rs b/crates/snowbound/src/settings.rs index 157eb3bcdf9762167e2c2c37b91a7b902a055a63..2f8722eba8f7ecf32c74becdf4c21a2ee4cbc5dc 100644 --- a/crates/snowbound/src/settings.rs +++ b/crates/snowbound/src/settings.rs @@ -307,18 +307,18 @@ impl crate::State { color_scheme: self.color_scheme, light_pages: self.light_pages, hide_spelling: self.hide_spelling, - ignore_markdown: self.view.editor.markdown.is_none(), + ignore_markdown: self.pane.view.editor.markdown.is_none(), search_scope: self.search.default, tags: (self.tags != canvas::editor::NoteTag::defaults()).then(|| self.tags.clone()), manual_updates: !self.updates.automatic(), ignore_pen_pressure: !self.pen_pressure, - ignore_grid: !self.view.snap_to_grid, - default_font: self.view.editor.default_font.clone(), + ignore_grid: !self.pane.view.snap_to_grid, + default_font: self.pane.view.editor.default_font.clone(), page_tabs_left: self.page_tabs_left, navigation_bar_right: self.navigation_bar_right, servers: self.servers.clone(), keys: crate::commands::Keymap::current().saved(), - recent: self.trail.recent.clone(), + recent: self.recent.clone(), passwords: self.passwords, notebook_theme: NotebookTheme(self.notebook_theme.clone()), live: self.live_options.clone(), diff --git a/crates/snowbound/src/sidebar.rs b/crates/snowbound/src/sidebar.rs index 5c4b0f2eccb5a986daa6181df9782a8fbca78175..336aee0ef34e3f4c443599de71000220b8ed9c3b 100644 --- a/crates/snowbound/src/sidebar.rs +++ b/crates/snowbound/src/sidebar.rs @@ -941,7 +941,7 @@ impl crate::State { }, ); crate::name(&mut self.ui, rows_id, "My Notebooks"); - let shown = match (&self.session, &self.locked) { + let shown = match (&self.session, &self.pane.locked) { (Some(session), _) => Some((&session.library, session.tabs[session.tab].path.as_str())), (None, Some(locked)) => Some((&locked.library, locked.path.as_str())), (None, None) => None, @@ -1444,7 +1444,7 @@ impl crate::State { read: impl FnOnce(&str, &std::path::Path) -> Result + Send + 'static, ) { let section = section.or_else(|| { - (self.trail.recent.iter()) + (self.recent.iter()) .find(|place| place.notebook == location) .map(|place| place.section.clone()) }); @@ -1477,8 +1477,8 @@ impl crate::State { return Err("This folder holds no notebook sections.".into()); }; let section = library.open_async(&path, notify).await?; - let (session, page) = crate::read_session(section, library, path, left)?; - Ok(crate::Loaded::Section(Box::new(session), page)) + let (session, space, page) = crate::read_session(section, library, path, left)?; + Ok(crate::Loaded::Section(Box::new(session), space, page)) }); } diff --git a/crates/snowbound/src/task_pane.rs b/crates/snowbound/src/task_pane.rs index 4c9f10502e6b21644d9bcc1254cdda86979cf722..7d53d4dbee93fdbda5018290bccae977a33e8d2f 100644 --- a/crates/snowbound/src/task_pane.rs +++ b/crates/snowbound/src/task_pane.rs @@ -445,7 +445,7 @@ impl State { /// Shows the next marked word in the Spelling pane; past the last, the check is complete. fn next_word(&mut self) { - match self.view.next_correction() { + match self.pane.view.next_correction() { Ok(Some((response, correction))) => { self.respond(response); self.correction = Some(correction); @@ -552,7 +552,7 @@ impl State { else { return; }; - let spelling = self.view.spelling.clone(); + let spelling = self.pane.view.spelling.clone(); let text = match (correction.repeated, pressed) { (_, _) if change => correction.suggestions.get(chosen).map(String::as_str), (true, [true, _]) => Some(""), @@ -566,7 +566,7 @@ impl State { } }; if let Some(text) = text { - match self.view.correct(&correction, text) { + match self.pane.view.correct(&correction, text) { Ok(response) => self.respond(response), Err(error) => eprintln!("{error}"), } @@ -936,7 +936,7 @@ impl State { let pages = &session.pages; let at = pages .iter() - .position(|(space, ..)| *space == session.space)?; + .position(|(space, ..)| *space == self.pane.showing.space)?; let base = pages.iter().map(|(.., level)| *level).min()?; let start = pages[..=at] .iter() diff --git a/crates/snowbound/src/templates.rs b/crates/snowbound/src/templates.rs index 7c82d7e511e0367072bdee2feb464ff54be5b327..703904294d6e9ac3588ef7d5190a7a5960c4e678 100644 --- a/crates/snowbound/src/templates.rs +++ b/crates/snowbound/src/templates.rs @@ -387,8 +387,8 @@ impl crate::State { let Some(session) = &self.session else { return; }; - let space = session.space; - let read_only = session.read_only(); + let space = self.pane.showing.space; + let read_only = session.read_only(&self.pane.showing); let Some(rect) = self.ui.laid_out(crate::page()) else { return; }; @@ -409,7 +409,7 @@ impl crate::State { ) { self.choose(crate::commands::Choice::Art(Some(name))); } - let editor = &self.view.editor; + let editor = &self.pane.view.editor; let blank = editor .visible_outlines() .chain(editor.caret_outline()) @@ -422,7 +422,7 @@ impl crate::State { return; }; let (color, rule_lines) = (editor.page_color(), editor.rule_lines()); - let viewport = self.view.viewport; + let viewport = self.pane.view.viewport; let scale = self.ui.scale(); let position = [ (viewport.origin[0] + start[0] * viewport.scale) / scale, diff --git a/crates/snowbound/src/themes.rs b/crates/snowbound/src/themes.rs index ee9128e2439a76db8c2bdad7ec1af17bdbef7b32..59bc430ef23dec7f49dbb8450149aa7867b9da60 100644 --- a/crates/snowbound/src/themes.rs +++ b/crates/snowbound/src/themes.rs @@ -120,7 +120,7 @@ impl State { let library = &session.library; library.catalog()?; let target = match scope { - Scope::Page => stored::Scope::page(self.view.editor.identity()?), + Scope::Page => stored::Scope::page(self.pane.view.editor.identity()?), Scope::Section => { stored::Scope::section(library.section_identity(&session.tabs[session.tab].path)?) } @@ -144,7 +144,7 @@ impl State { session .library .themes() - .effective(section, self.view.editor.identity()) + .effective(section, self.pane.view.editor.identity()) } /// The styles the gallery offers: the page's theme's, else OneNote 2010's. @@ -167,17 +167,17 @@ impl State { let sheet = (theme.as_ref()) .map(|theme| theme.sheet(self.section_color())) .unwrap_or_default(); - if !force && self.view.editor.styles == sheet { + if !force && self.pane.view.editor.styles == sheet { return Ok(()); } - self.view.editor.styles = sheet.clone(); + self.pane.view.editor.styles = sheet.clone(); let Some(session) = &self.session else { return Ok(()); }; - if theme.is_none() || session.read_only() { + if theme.is_none() || session.read_only(&self.pane.showing) { return Ok(()); } - let page = session.section.page(session.space)?; + let page = session.section.page(self.pane.showing.space)?; let ops = onestore::op::restyle(&page, &sheet)?; if ops.is_empty() { return Ok(()); diff --git a/crates/snowbound/src/undo.rs b/crates/snowbound/src/undo.rs index 022941d2f5a0220d65f8568aed46343b1d23a896..da9b06e8c9b25d4b18a633b869a4e880adb07e5e 100644 --- a/crates/snowbound/src/undo.rs +++ b/crates/snowbound/src/undo.rs @@ -958,13 +958,13 @@ impl State { /// Whether no action, page or command is on its way, which Undo and Redo wait for. pub(crate) fn settled(&self) -> bool { - self.undo.busy == 0 && self.switching.is_none() && self.commands.is_empty() + self.undo.busy == 0 && self.pane.switching.is_none() && self.commands.is_empty() } /// Whether Undo, or `redo` Redo, has something to take, or may once what is on its way /// lands. pub(crate) fn can_step(&self, redo: bool) -> bool { - let editor = &self.view.editor; + let editor = &self.pane.view.editor; !self.settled() || (if redo { editor.can_redo() @@ -998,9 +998,9 @@ impl State { /// user is, whichever came last. fn take(&mut self, redo: bool) -> Result<(), Box> { self.persist()?; - let depth = self.view.editor.history_depth(); + let depth = self.pane.view.editor.history_depth(); // A page kept in no notebook has only its own history. - let shown = self.session.as_ref().map(|session| session.space); + let shown = self.session.as_ref().map(|_| self.pane.showing.space); let next = match (self.here(), shown) { (Some(here), Some(page)) => self.undo.next(redo, page, depth, &here), _ => Some(Next::Edit), @@ -1008,10 +1008,10 @@ impl State { match next { None => {} Some(Next::Edit) => { - let response = self.view.undo(redo)?; + let response = self.pane.view.undo(redo)?; self.respond(response); if let Some(page) = shown - && self.view.editor.history_depth() != depth + && self.pane.view.editor.history_depth() != depth { self.undo.stepped(page, redo); } @@ -1117,11 +1117,11 @@ impl State { author: self.author.clone(), fresh: self.dated_page(None, "")?, }; - let shown = session.space; + let shown = self.pane.showing.space; let original = Arc::clone(&site.library); let proxy = self.proxy.clone(); self.undo.busy += 1; - self.switching = Some((None, web_time::Instant::now())); + self.pane.switching = Some((None, web_time::Instant::now())); self.load_async(move || async move { let applied = site.change(change, shown).await; let show = match &applied { @@ -1150,11 +1150,12 @@ impl State { } else { state.list(&library); if state + .pane .sectionless .as_ref() .is_some_and(|listed| lineage(listed)) { - state.sectionless = Some(library); + state.pane.sectionless = Some(library); } } } diff --git a/crates/snowbound/src/unread.rs b/crates/snowbound/src/unread.rs index f585124cce3cd949345a14df71b308827b5c8f4a..bfa61009fe681ddc0c4dbc07c75a142bf403fc55 100644 --- a/crates/snowbound/src/unread.rs +++ b/crates/snowbound/src/unread.rs @@ -274,8 +274,13 @@ impl State { let viewing = self.session.as_ref().and_then(|session| { let path = &session.tabs[session.tab].path; let section = session.library.section_identity(path)?; - (session.version.is_none() && !recycle::binned(path)) - .then(|| (session.library.location.clone(), section, session.space)) + (self.pane.showing.version.is_none() && !recycle::binned(path)).then(|| { + ( + session.library.location.clone(), + section, + self.pane.showing.space, + ) + }) }); let same = |(notebook, section, space, _): &(String, [u8; 16], ExGuid, bool)| { viewing.as_ref() == Some(&(notebook.clone(), *section, *space)) @@ -418,7 +423,7 @@ impl State { let at = session .pages .iter() - .position(|(space, ..)| *space == session.space); + .position(|(space, ..)| *space == self.pane.showing.space); let after = (session.pages.iter().skip(at.map_or(0, |at| at + 1))) .find(|(space, ..)| unread.contains(space)); if let Some((space, ..)) = after { diff --git a/crates/snowbound/src/web.rs b/crates/snowbound/src/web.rs index 0eebef76d5c8fce8e2cf0898d03a3bd9b1dad394..7ece906ba0e4eb61b7e78fc92df95a823cec6494 100644 --- a/crates/snowbound/src/web.rs +++ b/crates/snowbound/src/web.rs @@ -999,7 +999,7 @@ pub async fn start( if let Some(resume) = resumed().and_then(|resume| serde_json::from_str::(&resume).ok()) { for (axis, offset) in resume.offset.into_iter().enumerate() { - match state.view.scroll_to(axis, offset) { + match state.pane.view.scroll_to(axis, offset) { Ok(response) => state.respond(response), Err(error) => report(error), } @@ -1571,13 +1571,13 @@ fn turn(state: &mut State) -> Result<(), Box> { state.frame()?; remember_look(state); } - let space = state.session.as_ref().map(|session| session.space); + let space = state.session.as_ref().map(|_| state.pane.showing.space); if TYPED.take() || LOOKED.replace(space) != space { - fetch_fallbacks(&crate::page_text(&state.view.editor)); + fetch_fallbacks(&crate::page_text(&state.pane.view.editor)); } park_input(state); let now = Instant::now(); - let (repaint, blink) = state.view.blink(now); + let (repaint, blink) = state.pane.view.blink(now); let wake = state.ui.wake_at(); if repaint || wake.is_some_and(|wake| wake <= now) { state.window.request_redraw(); @@ -1709,7 +1709,7 @@ pub fn update_now(state: &mut State) { return RELOADING.set(false); }; let resume = Resume { - offset: state.view.viewport.origin.map(|origin| -origin), + offset: state.pane.view.viewport.origin.map(|origin| -origin), build, }; let reloading = reload_into(&serde_json::to_string(&resume).unwrap_or_default()); @@ -1737,8 +1737,8 @@ fn follow_updates(state: &mut State, now: Instant) -> Option { } let idle = LAST_INPUT.get().map_or(now, |last| last + IDLE); let quiet = !state.ui.popups_open() - && state.opening.is_none() - && state.loading == 0 + && state.pane.opening.is_none() + && state.pane.loading == 0 && matches!(state.media, crate::recording::Media::Idle); if update_ready() && (returned || idle <= now) && quiet { update_now(state); @@ -1783,7 +1783,7 @@ pub fn font_arrived(name: String, bytes: Vec) { families }; send(UserEvent::Then(Box::new(move |state| { - let families = register(&mut state.view.engine.fonts.collection, face.clone()); + let families = register(&mut state.pane.view.engine.fonts.collection, face.clone()); if let Ok(mut layouts) = state.layouts.lock() { register(&mut layouts.fonts.collection, face.clone()); } @@ -1792,7 +1792,7 @@ pub fn font_arrived(name: String, bytes: Vec) { } state.ui.fall_back_to(&families); state.renderer.clear_glyph_cache(); - let response = state.view.relayout()?; + let response = state.pane.view.relayout()?; state.respond(response); state.window.request_redraw(); Ok(()) @@ -1816,7 +1816,7 @@ pub fn dictionary_arrived(name: String, affix: String, words: String) { /// Parks the text area while the page takes no text, keeping it writable for the /// interface's fields. fn park_input(state: &State) { - if state.ui.focused() != Some(page()) || !state.view.accepts_text() { + if state.ui.focused() != Some(page()) || !state.pane.view.accepts_text() { place_input(0.0, 0.0, 1.0, state.ui.focused().is_some()); } }