305 lines
11 KiB
Rust
305 lines
11 KiB
Rust
//! 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<RowAddress>,
|
|
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<Opened>,
|
|
}
|
|
|
|
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<Message> {
|
|
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<Message> {
|
|
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<i32>) -> Task<Message> {
|
|
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<Task<Message>> {
|
|
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<Font = iced::Font> + '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;
|