//! One transient chooser for the currently mounted Inspector or review field. //! Browsing never calls a draft setter or semantic planner. use super::{App, Message, Page, Pending}; use crate::{ forms::{ Draft, reference_chooser::{self as chooser, Action, Close, Outcome, adapter, trigger}, }, navigation::InspectorTab, semantic_review::{self, RowAddress, RowEvent}, workflow, }; use iced::{Element, Rectangle, Task}; use oa_core::{FacetSelector, Id, Layer}; #[derive(Debug, Clone, PartialEq, Eq)] enum Host { Inspector, Review { address: Option, facet: FacetSelector, }, } impl Host { fn message(&self, event: chooser::Event) -> Message { match self { Self::Inspector => Message::FieldReferenceEvent(event), Self::Review { address, facet } => { Message::Workflow(workflow::Event::semantic(semantic_review::Event::Row { address: *address, facet: Some(facet.clone()), event: RowEvent::ReferenceEvent(event), })) } } } } struct Opened { host: Host, request: adapter::Open, document_session: Id, page: Page, layer: Layer, chooser: chooser::State, } #[derive(Default)] pub(super) struct State { opened: Option, } impl App { pub(crate) fn reference_input_active(&self) -> bool { self.reference_authoring.opened.is_some() } fn reference_draft(&self, host: &Host, path: &str) -> Option<&Draft> { match host { Host::Inspector if self.workflow.is_none() && self.inspector_tab == InspectorTab::Properties => { self.draft.as_ref() } Host::Review { address, facet } => match self.workflow.as_ref()? { workflow::Workflow::Semantic(state) => { state.reference_draft(self.project(), *address, facet, path) } _ => None, }, _ => None, } } fn reference_available(&self) -> bool { !self.busy && !self.hosted.visible && self.architecture.is_none() && self.model_menu.is_none() && self.file_prompt.is_none() && !self.delete_pending && self.pending.is_none() && !self.source_dirty() && !self.behavior.has_pending() && !matches!(self.page, Page::Home | Page::Guide | Page::Text) } fn reference_valid(&self, opened: &Opened) -> bool { self.reference_available() && self.canvas_document == opened.document_session && self.page == opened.page && self.layer == opened.layer && self.document.revision() == opened.chooser.stamp().context.revision && self.project().id == opened.request.document && self .reference_draft(&opened.host, &opened.request.path) .is_some_and(|draft| draft.reference_matches(&opened.request)) } pub(super) fn reconcile_reference_chooser(&mut self) { if self .reference_authoring .opened .as_ref() .is_some_and(|opened| !self.reference_valid(opened)) { self.reference_authoring .opened .as_mut() .unwrap() .chooser .invalidate(); self.reference_authoring.opened = None; } } fn open_reference(&mut self, host: Host, request: adapter::Open) -> Task { if !self.reference_available() { return Task::none(); } let Some(draft) = self.reference_draft(&host, &request.path) else { return Task::none(); }; let Ok(chooser) = draft.reference_catalog(self.project(), &request, self.document.revision()) else { return Task::none(); }; self.reference_authoring.opened = Some(Opened { host, request, document_session: self.canvas_document, page: self.page, layer: self.layer, chooser, }); Task::none() } fn reference_event(&mut self, host: Host, event: chooser::Event) -> Task { let Some(mut opened) = self.reference_authoring.opened.take() else { return Task::none(); }; if !self.reference_valid(&opened) { return Task::none(); } if opened.host != host { self.reference_authoring.opened = Some(opened); return Task::none(); } match opened.chooser.update(event) { Outcome::Ignored | Outcome::Browsed => { self.reference_authoring.opened = Some(opened); Task::none() } Outcome::Closed(reason) => self.reference_focus( &opened, match reason { Close::Escape => Some(0), Close::Tab { backwards } => Some(if backwards { -1 } else { 1 }), Close::Outside | Close::WindowChanged => None, }, ), Outcome::Write { context, value } => { let intent = adapter::Set { context, generation: opened.request.generation, value, }; let result = match &opened.host { Host::Inspector => self .draft .as_mut() .ok_or_else(|| "The property editor closed.".to_owned()) .and_then(|draft| draft.reference_set(self.document.project(), &intent)), Host::Review { address, facet } => match self.workflow.as_mut() { Some(workflow::Workflow::Semantic(state)) => { state.set_reference(self.document.project(), *address, facet, intent) } _ => Err("The review editor closed.".into()), }, }; if let Err(error) = result { self.status = error; } // A scoped write rotates field generation. The task targets the // newly rendered field, while a later structural edit makes it inert. self.reference_focus(&opened, Some(0)) } } } fn reference_focus(&self, opened: &Opened, step: Option) -> Task { let Some(step) = step else { return Task::none(); }; let Some(draft) = self.reference_draft(&opened.host, &opened.request.path) else { return Task::none(); }; if draft.reference_session() != opened.request.session { return Task::none(); } trigger::focus( draft.reference_session(), draft.reference_generation(), &opened.request.path, step, ) } /// Called before generic modal dispatch: the review owns a modal, and this /// chooser owns only its active field. All browser events stop here. pub(super) fn reference_message(&mut self, message: &Message) -> Option> { self.reconcile_reference_chooser(); match message { Message::FieldReferenceOpen(request) => { Some(self.open_reference(Host::Inspector, request.clone())) } Message::FieldReferenceEvent(event) => { Some(self.reference_event(Host::Inspector, event.clone())) } Message::Workflow(workflow::Event::Semantic(review_event)) => { let semantic_review::Event::Row { address, facet: Some(facet), event, } = review_event.as_ref() else { return None; }; let host = Host::Review { address: *address, facet: facet.clone(), }; match event { RowEvent::ReferenceOpen(request) => { Some(self.open_reference(host, request.clone())) } RowEvent::ReferenceEvent(event) => { Some(self.reference_event(host, event.clone())) } _ => None, } } Message::Escape | Message::Request(Pending::Close(_)) if self.reference_authoring.opened.is_some() => { let opened = self.reference_authoring.opened.as_ref().unwrap(); let host = opened.host.clone(); let event = opened.chooser.event(Action::Close(Close::Escape)); Some(self.reference_event(host, event)) } // Global shortcuts are also emitted for keys captured by the search // input. They must not apply drafts, navigate, or undo the model. Message::Save(_) | Message::ShortcutUndo(_) | Message::Request(Pending::Open | Pending::New) | Message::SourceNavigation(_) | Message::Page(Page::Guide) if self.reference_authoring.opened.is_some() => { Some(Task::none()) } _ => None, } } pub(super) fn reference_overlay< 'a, R: iced::advanced::text::Renderer + 'a, >( &'a self, content: Element<'a, Message, iced::Theme, R>, ) -> Element<'a, Message, iced::Theme, R> { let opened = self .reference_authoring .opened .as_ref() .filter(|opened| self.reference_valid(opened)); let host = opened .map(|opened| opened.host.clone()) .unwrap_or(Host::Inspector); let focus_scope = if self.reference_available() { match self.workflow.as_ref() { None if self.inspector_tab == InspectorTab::Properties => { self.draft.as_ref().map(|draft| trigger::FormScope { session: draft.reference_session(), modal: false, }) } Some(workflow::Workflow::Semantic(state)) => state .reference_form(self.project()) .map(|draft| trigger::FormScope { session: draft.reference_session(), modal: true, }), _ => None, } } else { None }; chooser::widget::Popup::new( content, opened.map(|opened| &opened.chooser), opened.map_or(Rectangle::default(), |opened| opened.request.anchor), move |event| host.message(event), ) .focus_scope(focus_scope) .into() } } #[cfg(test)] mod tests;