Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/source_draft.rs
T

1035 lines
39 KiB
Rust

//! A prerequisite belongs to its pending consumer, never the accepted catalog.
use iced::{
Element as UiElement, Fill,
widget::{button, column, container, row, scrollable, text, text_input},
};
use oa_core::*;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const NAME_INPUT: &str = "behavior-source-name";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Scope {
Lifeline {
owner: Id,
viewpoint: ScenarioViewpoint,
},
Work {
owner: Id,
},
Machine {
layer: Layer,
},
}
impl Scope {
fn owner(self) -> Option<Id> {
match self {
Self::Lifeline { owner, .. } | Self::Work { owner } => Some(owner),
Self::Machine { .. } => None,
}
}
fn layer(self, p: &Project) -> Result<Layer, String> {
match self {
Self::Machine { layer } => Ok(layer),
_ => p
.layer_of(self.owner().unwrap())
.ok_or("The consumer owner no longer exists.".into()),
}
}
fn kinds(self, layer: Layer) -> Vec<Kind> {
match self {
Self::Work { .. } => vec![if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
}],
Self::Lifeline {
viewpoint: ScenarioViewpoint::OperationalActivity,
..
} => vec![Kind::Activity],
Self::Lifeline {
viewpoint: ScenarioViewpoint::Functional,
..
} => vec![Kind::Function],
Self::Lifeline {
viewpoint: ScenarioViewpoint::ParticipantInteraction,
..
} => vec![Kind::Entity, Kind::Actor, Kind::Role],
Self::Lifeline {
viewpoint: ScenarioViewpoint::BoundaryInterface,
..
} => vec![Kind::System, Kind::SystemActor],
Self::Machine { .. } if layer == Layer::Oa => vec![Kind::Entity, Kind::Actor],
Self::Machine { .. } => vec![Kind::System, Kind::SystemActor],
_ => vec![],
}
}
}
#[derive(Debug, Clone)]
pub enum Event {
Open(Id),
Edit(Id, Edit),
}
#[derive(Debug, Clone)]
pub enum Edit {
Name(String),
Kind(Kind),
Parent(Option<Id>),
Search(String),
Page(usize),
Back,
Use,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Draft {
token: Id,
document: Id,
scope: Scope,
layer: Layer,
owner: Option<Element>,
id: Id,
kind: Option<Kind>,
name: String,
parent: Option<Element>,
search: String,
page: usize,
identities: Vec<Id>,
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct State {
editing: Option<Draft>,
staged: Option<Draft>,
#[serde(default)]
engaged: bool,
}
impl State {
pub fn check_scope(&self, expected: Scope, selected: Option<Id>) -> Result<(), String> {
if self
.editing
.as_ref()
.or_else(|| self.staged.as_ref().filter(|d| Some(d.id) == selected))
.is_some_and(|d| d.scope != expected)
{
return Err("The recovered source request belongs to a different consumer context. Go Back or cancel this draft.".into());
}
Ok(())
}
pub fn dismiss(&mut self, event: &Event) -> Result<bool, String> {
if let Event::Edit(token, Edit::Back) = event {
if self.editing.as_ref().is_none_or(|d| d.token != *token) {
return Err("This Back action belongs to a previous source form.".into());
}
self.editing = None;
self.engaged = true;
return Ok(true);
}
Ok(false)
}
pub fn engaged(&self) -> bool {
self.engaged || self.editing.is_some() || self.staged.is_some()
}
pub fn is_open(&self) -> bool {
self.editing.is_some()
}
pub fn back(&mut self) -> bool {
self.editing.take().is_some()
}
pub fn label(&self, selected: Option<Id>) -> Option<String> {
self.staged
.as_ref()
.filter(|d| Some(d.id) == selected)
.map(|d| {
format!(
"New · {} · {}",
d.kind.map_or("Choose kind".into(), |k| k.to_string()),
d.name
)
})
}
pub fn update(
&mut self,
p: &Project,
scope: Scope,
invocation: Id,
event: Event,
) -> Result<Option<Id>, String> {
if self.dismiss(&event)? {
return Ok(None);
}
match event {
Event::Open(token) => {
if token != invocation || self.is_open() {
return Err("This source request is stale. Use the current form.".into());
}
let layer = scope.layer(p)?;
let kinds = scope.kinds(layer);
if kinds.is_empty() {
return Err("Create new sources requires a strict scenario viewpoint; use an existing source here.".into());
}
self.engaged = true;
self.editing = Some(Draft {
token: Id::new_v4(),
document: p.id,
scope,
layer,
owner: scope.owner().and_then(|id| p.elements.get(&id).cloned()),
id: Id::new_v4(),
kind: (kinds.len() == 1).then_some(kinds[0]),
name: String::new(),
parent: None,
search: String::new(),
page: 0,
identities: (0..match scope {
Scope::Work { owner } => p.children(owner).count() + 4,
Scope::Lifeline { .. } => 1,
Scope::Machine { .. } => 0,
})
.map(|_| Id::new_v4())
.collect(),
});
Ok(None)
}
Event::Edit(token, edit) => {
let d = self
.editing
.as_mut()
.filter(|d| d.token == token && d.scope == scope && d.document == p.id)
.ok_or("This source response is stale; return to the current form.")?;
match edit {
Edit::Back => {
self.editing = None;
Ok(None)
}
Edit::Use => {
d.command(p)?;
let id = d.id;
self.staged = self.editing.take();
Ok(Some(id))
}
Edit::Name(name) => {
d.name = name;
Ok(None)
}
Edit::Kind(kind) => {
if !scope.kinds(d.layer).contains(&kind) {
return Err("Choose an explicitly supported source kind.".into());
}
d.kind = Some(kind);
d.parent = None;
d.page = 0;
Ok(None)
}
Edit::Parent(parent) => {
let kind = d.kind.ok_or("Choose the source kind first.")?;
if !validation::can_parent(p, kind, d.layer, parent) {
return Err("Choose a valid semantic parent.".into());
}
d.parent = parent.map(|id| p.elements[&id].clone());
Ok(None)
}
Edit::Search(query) => {
d.search = query;
d.page = 0;
Ok(None)
}
Edit::Page(page) => {
d.page = page;
Ok(None)
}
}
}
}
}
pub fn preview(&self, p: &Project, selected: Option<Id>) -> Result<Option<Project>, String> {
if self.is_open() {
return Err("Use this new source or go Back before creating the consumer.".into());
}
self.staged
.as_ref()
.filter(|d| Some(d.id) == selected)
.map(|d| execute_batch(p, &[d.command(p)?]).map_err(|e| e.to_string()))
.transpose()
}
pub fn combine(
&self,
p: &Project,
selected: Option<Id>,
command: Command,
) -> Result<Command, String> {
let Some(d) = self.staged.as_ref().filter(|d| Some(d.id) == selected) else {
return Ok(command);
};
let preview = execute_batch(p, &[d.command(p)?]).map_err(|e| e.to_string())?;
let mut commands = match command {
Command::Batch(commands) => commands,
other => vec![other],
};
let fresh: Vec<_> = commands
.iter()
.filter(|_| !matches!(d.scope, Scope::Machine { .. }))
.filter_map(|c| match c {
Command::CreateElement(e) => Some(e.id),
Command::CreateRelation(r) => Some(r.id),
_ => None,
})
.collect();
if fresh.len() > d.identities.len() {
return Err(
"The path grew beyond this draft. Return and create the source again.".into(),
);
}
let replacements: BTreeMap<_, _> = fresh
.into_iter()
.zip(d.identities.iter().copied())
.collect();
if replacements.values().any(|id| preview.contains(*id)) {
return Err(
"A reserved consumer identity is already used. Start a fresh source draft.".into(),
);
}
let remap = |id: &mut Id| {
if let Some(new) = replacements.get(id) {
*id = *new;
}
};
for c in &mut commands {
match c {
Command::CreateElement(e) => {
remap(&mut e.id);
references::visit_element_refs(e, remap);
e.alias = preview.fresh_alias(&e.name, "contribution", e.id);
}
Command::CreateRelation(r) => {
remap(&mut r.id);
references::visit_relation_refs(r, remap);
r.alias = preview.fresh_alias(&r.name, "path", r.id);
}
Command::EditRelation(_, r) => references::visit_relation_refs(r, remap),
Command::SetSteps(_, steps) => steps.iter_mut().for_each(remap),
// Machine commands already carry their reserved identities.
Command::CreateDiagram(_) | Command::Layout(_) => {}
_ => return Err("Unsupported prerequisite consumer operation.".into()),
}
}
commands.insert(0, d.command(p)?);
let command = super::topology::unique_batch(p, commands);
execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
Ok(command)
}
pub fn view<'a, R: iced::advanced::text::Renderer + 'a>(
&'a self,
p: &'a Project,
) -> Option<UiElement<'a, Event, iced::Theme, R>> {
let d = self.editing.as_ref()?;
let event = move |edit| Event::Edit(d.token, edit);
let kinds = d.scope.kinds(d.layer);
let determined = kinds.len() == 1 && d.kind == kinds.first().copied();
let mut fields = column![
text(if determined {
format!("New {} · {}", kinds[0], d.layer)
} else {
format!("New source · {}", d.layer)
})
.size(14),
text_input("Source name (independent of the consumer name)", &d.name)
.id(NAME_INPUT)
.on_input(move |v| event(Edit::Name(v)))
.size(14)
.padding(6),
]
.spacing(4);
if !determined {
let mut choices = row![].spacing(6);
for kind in kinds {
choices = choices.push(
container(
button(text(kind.to_string()).size(14))
.padding(4)
.on_press(event(Edit::Kind(kind)))
.style(if d.kind == Some(kind) {
button::primary
} else {
button::secondary
}),
)
.id(format!("behavior-source-kind-{kind:?}")),
);
}
fields = fields.push(choices.wrap());
}
fields = fields.push(
container(
text(format!(
"Semantic parent: {}",
d.parent
.as_ref()
.map_or(format!("{} layer root", d.layer), |e| format!(
"{} · {} · @{}",
e.name,
e.kind(),
e.alias
))
))
.size(12),
)
.id("behavior-source-parent"),
);
fields = fields
.push(
text(match d.scope {
Scope::Lifeline { owner, viewpoint } => {
format!("For a lifeline in {} · {:?}", p.named(owner), viewpoint)
}
Scope::Work { owner } => format!("For a contribution in {}", p.named(owner)),
Scope::Machine { .. } => "Owner for the pending state machine".into(),
})
.size(12),
)
.push(
text("The parent form is retained. This source is created only with its consumer.")
.size(12),
);
if let Some(kind) = d.kind {
fields = fields.push(button("Use layer root").on_press_maybe(
validation::can_parent(p, kind, d.layer, None).then(|| event(Edit::Parent(None))),
));
fields = fields.push(
text_input("Find a semantic parent (optional)", &d.search)
.on_input(move |v| event(Edit::Search(v)))
.padding(6),
);
let query = d.search.to_lowercase();
let mut parents: Vec<_> = p
.elements
.values()
.filter(|e| validation::can_parent(p, kind, d.layer, Some(e.id)))
.map(|e| (e.id, super::creation::reference_label(p, e.id)))
.filter(|(_, label)| label.to_lowercase().contains(&query))
.collect();
parents.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
let page = d.page.min(parents.len().saturating_sub(1) / 6);
for (id, label) in parents.iter().skip(page * 6).take(6) {
fields = fields.push(
button(text(label.clone()).size(12))
.on_press(event(Edit::Parent(Some(*id))))
.width(Fill),
);
}
if parents.len() > 6 {
fields = fields.push(
row![
button("Previous").on_press_maybe(
(page > 0).then(|| event(Edit::Page(page.saturating_sub(1))))
),
text(format!("{} / {}", page + 1, parents.len().div_ceil(6))),
button("Next").on_press_maybe(
((page + 1) * 6 < parents.len()).then(|| event(Edit::Page(page + 1)))
)
]
.spacing(6),
);
}
}
let ready = d.command(p);
if let Err(error) = &ready {
fields = fields.push(text(error.clone()).size(12));
}
Some(
container(
column![
scrollable(fields).id("behavior-source-fields").height(Fill),
container(
row![
button("Back · Esc").on_press(event(Edit::Back)),
button("Use this new source")
.on_press_maybe(ready.is_ok().then(|| event(Edit::Use)))
]
.spacing(8)
)
.id("behavior-source-actions")
]
.spacing(10),
)
.padding(12)
.height(Fill)
.width(Fill)
.into(),
)
}
}
impl Draft {
fn command(&self, p: &Project) -> Result<Command, String> {
if self.document != p.id
|| self.scope.layer(p)? != self.layer
|| self
.owner
.as_ref()
.is_some_and(|e| p.elements.get(&e.id) != Some(e))
{
return Err(
"The source request's document or consumer changed. Go Back and reopen Create new."
.into(),
);
}
let kind = self
.kind
.filter(|kind| self.scope.kinds(self.layer).contains(kind))
.ok_or("Choose the source kind explicitly.")?;
if self.name.trim().is_empty() {
return Err("Enter the source name.".into());
}
if p.contains(self.id) {
return Err(
"The reserved source identity is already used. Go Back and start again.".into(),
);
}
let parent = self.parent.as_ref().map(|e| e.id);
if self
.parent
.as_ref()
.is_some_and(|e| p.elements.get(&e.id) != Some(e))
|| !validation::can_parent(p, kind, self.layer, parent)
{
return Err("The semantic parent changed. Choose its intended parent again.".into());
}
let e = Element {
id: self.id,
alias: p.fresh_alias(&self.name, "source", self.id),
name: self.name.clone(),
description: String::new(),
parent,
body: ElementBody::new(kind, self.layer)
.ok_or("This source kind is unavailable in this layer.")?,
};
let command = Command::CreateElement(e);
execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
Ok(command)
}
}
#[cfg(all(test, debug_assertions))]
mod layout_tests {
use super::*;
use iced::advanced::{Layout, Shell, graphics, layout, renderer, text as shaped, widget};
use iced::{Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, Vector};
// Use Iced's real paragraph shaping/measurement without a window or GPU.
// The unit renderer's zero-size paragraphs cannot detect this regression.
struct FontLayout;
impl renderer::Renderer for FontLayout {
fn start_layer(&mut self, _: Rectangle) {}
fn end_layer(&mut self) {}
fn start_transformation(&mut self, _: Transformation) {}
fn end_transformation(&mut self) {}
fn reset(&mut self, _: Rectangle) {}
fn fill_quad(&mut self, _: renderer::Quad, _: impl Into<Background>) {}
fn allocate_image(
&mut self,
handle: &iced::advanced::image::Handle,
callback: impl FnOnce(
Result<iced::advanced::image::Allocation, iced::advanced::image::Error>,
) + Send
+ 'static,
) {
renderer::Renderer::allocate_image(&mut (), handle, callback);
}
}
impl shaped::Renderer for FontLayout {
type Font = Font;
type Paragraph = graphics::text::Paragraph;
type Editor = ();
const ICON_FONT: Font = Font::DEFAULT;
const CHECKMARK_ICON: char = '0';
const ARROW_DOWN_ICON: char = '0';
const SCROLL_UP_ICON: char = '0';
const SCROLL_DOWN_ICON: char = '0';
const SCROLL_LEFT_ICON: char = '0';
const SCROLL_RIGHT_ICON: char = '0';
const ICED_LOGO: char = '0';
fn default_font(&self) -> Font {
Font::DEFAULT
}
fn default_size(&self) -> Pixels {
Pixels(16.)
}
fn fill_paragraph(&mut self, _: &Self::Paragraph, _: Point, _: Color, _: Rectangle) {}
fn fill_editor(&mut self, _: &Self::Editor, _: Point, _: Color, _: Rectangle) {}
fn fill_text(&mut self, _: shaped::Text, _: Point, _: Color, _: Rectangle) {}
}
#[derive(Default)]
struct Geometry {
fields: Option<Rectangle>,
content: Option<Rectangle>,
name: Option<Rectangle>,
parent: Option<Rectangle>,
actions: Option<Rectangle>,
kinds: BTreeMap<String, Rectangle>,
text: Vec<(String, Rectangle)>,
focus_name: bool,
}
impl widget::Operation for Geometry {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
visit(self);
}
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
if id == Some(&widget::Id::new("behavior-source-actions")) {
self.actions = Some(bounds);
}
if id == Some(&widget::Id::new("behavior-source-parent")) {
self.parent = Some(bounds);
}
for kind in [
Kind::Entity,
Kind::Actor,
Kind::Role,
Kind::System,
Kind::SystemActor,
Kind::Activity,
Kind::Function,
] {
if id == Some(&widget::Id::from(format!("behavior-source-kind-{kind:?}"))) {
self.kinds.insert(kind.to_string(), bounds);
}
}
}
fn scrollable(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
content: Rectangle,
offset: Vector,
_: &mut dyn widget::operation::Scrollable,
) {
if id == Some(&widget::Id::new("behavior-source-fields")) {
assert_eq!(
offset,
Vector::ZERO,
"Initial fields must be visible without scrolling"
);
self.fields = Some(bounds);
self.content = Some(content);
}
}
fn text_input(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
_: &mut dyn widget::operation::TextInput,
) {
if id == Some(&widget::Id::new(NAME_INPUT)) {
self.name = Some(bounds);
}
}
fn focusable(
&mut self,
id: Option<&widget::Id>,
_: Rectangle,
state: &mut dyn widget::operation::Focusable,
) {
if self.focus_name && id == Some(&widget::Id::new(NAME_INPUT)) {
state.focus();
}
}
fn text(&mut self, _: Option<&widget::Id>, bounds: Rectangle, text: &str) {
self.text.push((text.into(), bounds));
}
}
fn contains(outer: Rectangle, inner: Rectangle) -> bool {
inner.width > 0.
&& inner.height > 0.
&& inner.x >= outer.x - 0.1
&& inner.y >= outer.y - 0.1
&& inner.x + inner.width <= outer.x + outer.width + 0.1
&& inner.y + inner.height <= outer.y + outer.height + 0.1
}
fn host<'a>(
state: &'a State,
p: &'a Project,
caption: &str,
) -> UiElement<'a, Event, iced::Theme, FontLayout> {
iced::widget::column![text(caption.to_owned()).size(12), state.view(p).unwrap()]
.spacing(6)
.height(Fill)
.width(Fill)
.into()
}
fn geometry(
view: &mut UiElement<'_, Event, iced::Theme, FontLayout>,
tree: &mut widget::Tree,
size: Size,
) -> (layout::Node, Geometry) {
let node =
view.as_widget_mut()
.layout(tree, &FontLayout, &layout::Limits::new(Size::ZERO, size));
let mut geometry = Geometry::default();
view.as_widget_mut()
.operate(tree, Layout::new(&node), &FontLayout, &mut geometry);
(node, geometry)
}
fn fixture(layer: Layer, viewpoint: Option<ScenarioViewpoint>, work: bool) -> (Project, State) {
let mut p = Project::empty();
let scope = if work {
let owner = p
.add_element(
if layer == Layer::Oa {
Kind::Process
} else {
Kind::FunctionalChain
},
layer,
"Consumer",
None,
)
.unwrap();
Scope::Work { owner }
} else if let Some(viewpoint) = viewpoint {
let owner = p
.add_element(
Kind::Scenario,
layer,
"Incident coordination with a long contextual heading 雨 Ω",
None,
)
.unwrap();
if let Some(SharedBody::Scenario { viewpoint: v, .. }) =
p.elements.get_mut(&owner).unwrap().shared_mut()
{
*v = viewpoint;
}
Scope::Lifeline { owner, viewpoint }
} else {
Scope::Machine { layer }
};
let mut state = State::default();
let invocation = Id::new_v4();
state
.update(&p, scope, invocation, Event::Open(invocation))
.unwrap();
(p, state)
}
#[test]
fn contextual_source_layout_initial_essentials_fit_short_composed_host_with_real_paragraphs() {
for (layer, viewpoint, work) in [
(
Layer::Oa,
Some(ScenarioViewpoint::OperationalActivity),
false,
),
(Layer::Sa, Some(ScenarioViewpoint::Functional), false),
(
Layer::Oa,
Some(ScenarioViewpoint::ParticipantInteraction),
false,
),
(Layer::Sa, Some(ScenarioViewpoint::BoundaryInterface), false),
(Layer::Oa, None, false),
(Layer::Sa, None, false),
(Layer::Oa, None, true),
(Layer::Sa, None, true),
] {
let (p, state) = fixture(layer, viewpoint, work);
let raw = serde_json::to_value(&state).unwrap();
for width in [340., 500.] {
// The retained caption + 6px gap leaves approximately the observed 194px card.
let size = Size::new(width, 215.);
let mut view = host(&state, &p, "Consumer name: Assessment lifeline");
let mut tree = widget::Tree::new(view.as_widget());
let (_, g) = geometry(&mut view, &mut tree, size);
let fields = g.fields.unwrap();
assert!(
(125. ..=132.).contains(&fields.height),
"Measured field viewport: {fields:?}"
);
assert!(
contains(fields, g.name.unwrap()),
"Name clipped: {layer:?}/{viewpoint:?}/{work}/{width}"
);
assert!(
contains(fields, g.parent.unwrap()),
"Parent clipped: {layer:?}/{viewpoint:?}/{work}/{width}"
);
assert!(contains(
Rectangle::new(Point::ORIGIN, size),
g.actions.unwrap()
));
assert!(fields.y + fields.height <= g.actions.unwrap().y);
let expected = state.editing.as_ref().unwrap().scope.kinds(layer);
assert_eq!(
g.kinds.len(),
if expected.len() == 1 {
0
} else {
expected.len()
}
);
for bounds in g.kinds.values() {
assert!(
contains(fields, *bounds),
"Kind clipped: {bounds:?} in {fields:?}"
);
}
let parent_text = g
.text
.iter()
.find(|(s, _)| s.starts_with("Semantic parent:"))
.unwrap();
assert_eq!(
parent_text.0,
format!("Semantic parent: {layer} layer root")
);
assert!(
parent_text.1.height > 0.,
"Paragraph measurement must not be null"
);
let explanation = g
.text
.iter()
.find(|(s, _)| s.starts_with("The parent form"))
.unwrap();
assert!(explanation.1.y >= g.parent.unwrap().y + g.parent.unwrap().height);
}
assert_eq!(
serde_json::to_value(&state).unwrap(),
raw,
"Layout must not choose a kind or mutate the draft"
);
if state.editing.as_ref().unwrap().scope.kinds(layer).len() == 1 {
let mut incomplete = state.clone();
incomplete.editing.as_mut().unwrap().kind = None;
let mut view = host(&incomplete, &p, "Consumer name: Assessment lifeline");
let mut tree = widget::Tree::new(view.as_widget());
let (_, g) = geometry(&mut view, &mut tree, Size::new(340., 215.));
assert_eq!(
g.kinds.len(),
1,
"An incomplete recovery still needs an explicit repair choice"
);
assert!(contains(
g.fields.unwrap(),
*g.kinds.values().next().unwrap()
));
assert!(contains(g.fields.unwrap(), g.name.unwrap()));
assert_eq!(incomplete.editing.as_ref().unwrap().kind, None);
}
}
}
#[test]
fn contextual_source_layout_long_parent_wraps_after_visible_name_without_losing_raw_data() {
let (mut p, mut state) = fixture(Layer::Oa, None, true);
let parent = p
.add_element(
Kind::Activity,
Layer::Oa,
"Northern operations coordination — exact parent 雨",
None,
)
.unwrap();
let d = state.editing.as_ref().unwrap();
let token = d.token;
let scope = d.scope;
let invocation = Id::new_v4();
state
.update(
&p,
scope,
invocation,
Event::Edit(token, Edit::Parent(Some(parent))),
)
.unwrap();
state
.update(
&p,
scope,
invocation,
Event::Edit(token, Edit::Name(" Distinct source 雨 ".into())),
)
.unwrap();
let raw = serde_json::to_value(&state).unwrap();
for width in [340., 500.] {
let size = Size::new(width, 235.);
let mut view = host(
&state,
&p,
"Consumer name: Coordination with a long independent label 雨 Ω",
);
let mut tree = widget::Tree::new(view.as_widget());
let (_, g) = geometry(&mut view, &mut tree, size);
assert!(contains(g.fields.unwrap(), g.name.unwrap()));
assert!(contains(g.fields.unwrap(), g.parent.unwrap()));
assert!(contains(
Rectangle::new(Point::ORIGIN, size),
g.actions.unwrap()
));
let summary = g
.text
.iter()
.find(|(s, _)| s.starts_with("Semantic parent:"))
.unwrap();
assert!(summary.0.contains(&p.elements[&parent].name));
assert!(summary.0.contains(&p.elements[&parent].alias));
assert!(
summary.1.height > 15.,
"Long parent should exercise real wrapping"
);
}
assert_eq!(serde_json::to_value(&state).unwrap(), raw);
assert_eq!(
state.editing.as_ref().unwrap().name,
" Distinct source 雨 "
);
let (mut p, mut state) = fixture(
Layer::Oa,
Some(ScenarioViewpoint::ParticipantInteraction),
false,
);
let parent = p
.add_element(
Kind::Entity,
Layer::Oa,
// Eight repetitions need not exceed a whole viewport with the
// platform's measured font. Exercise substantial real overflow.
&"Northern operational responsibility 雨 ".repeat(32),
None,
)
.unwrap();
let d = state.editing.as_ref().unwrap();
let token = d.token;
let scope = d.scope;
state
.update(
&p,
scope,
Id::new_v4(),
Event::Edit(token, Edit::Kind(Kind::Actor)),
)
.unwrap();
state
.update(
&p,
scope,
Id::new_v4(),
Event::Edit(token, Edit::Parent(Some(parent))),
)
.unwrap();
let raw = serde_json::to_value(&state).unwrap();
let recovered: State = serde_json::from_value(raw.clone()).unwrap();
let mut view = host(&recovered, &p, "Consumer name: Assessment lifeline");
let mut tree = widget::Tree::new(view.as_widget());
let (_, g) = geometry(&mut view, &mut tree, Size::new(340., 215.));
let fields = g.fields.unwrap();
assert!(contains(fields, g.name.unwrap()));
assert_eq!(g.kinds.len(), 3);
for bounds in g.kinds.values() {
assert!(
contains(fields, *bounds),
"Long parent must not hide a semantic choice"
);
assert!(bounds.y + bounds.height <= g.parent.unwrap().y);
}
// Arbitrary authored detail may scroll, but its label and first line are
// visible after the required kind controls, never in front of them.
let parent_bounds = g.parent.unwrap();
assert!(parent_bounds.y + 16. <= fields.y + fields.height);
assert!(
parent_bounds.height > fields.height,
"Exercise genuinely overflowing parent text: {parent_bounds:?} in {fields:?}"
);
let content = g.content.unwrap();
assert!(
content.height > fields.height && contains(content, parent_bounds),
"Full parent must remain in scrollable content: {parent_bounds:?}, {content:?}, {fields:?}"
);
let summary = g
.text
.iter()
.find(|(s, _)| s.starts_with("Semantic parent:"))
.unwrap();
assert!(summary.0.contains(&p.elements[&parent].name));
assert!(summary.0.contains(&p.elements[&parent].alias));
assert!(contains(
Rectangle::new(Point::ORIGIN, Size::new(340., 215.)),
g.actions.unwrap()
));
assert_eq!(serde_json::to_value(&recovered).unwrap(), raw);
}
#[test]
fn contextual_source_layout_visible_kind_click_keeps_choice_explicit_and_name_input_works() {
let (p, mut state) = fixture(
Layer::Oa,
Some(ScenarioViewpoint::ParticipantInteraction),
false,
);
let original = p.clone();
let token = state.editing.as_ref().unwrap().token;
let scope = state.editing.as_ref().unwrap().scope;
assert_eq!(state.editing.as_ref().unwrap().kind, None);
let size = Size::new(340., 215.);
let mut view = host(&state, &p, "Consumer name: Assessment lifeline");
let mut tree = widget::Tree::new(view.as_widget());
let (node, g) = geometry(&mut view, &mut tree, size);
let mut messages = Vec::new();
for kind in [Kind::Entity, Kind::Actor, Kind::Role] {
let bounds = g.kinds[&kind.to_string()];
assert!(contains(g.fields.unwrap(), bounds));
for action in [
iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left),
iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left),
] {
view.as_widget_mut().update(
&mut tree,
&iced::Event::Mouse(action),
Layout::new(&node),
iced::mouse::Cursor::Available(bounds.center()),
&FontLayout,
&mut iced::advanced::clipboard::Null,
&mut Shell::new(&mut messages),
&Rectangle::new(Point::ORIGIN, size),
);
}
assert!(
matches!(messages.pop(), Some(Event::Edit(id, Edit::Kind(k))) if id == token && k == kind)
);
}
let mut focus = Geometry {
focus_name: true,
..Geometry::default()
};
view.as_widget_mut()
.operate(&mut tree, Layout::new(&node), &FontLayout, &mut focus);
let event = iced::Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: iced::keyboard::Key::Character("Ω".into()),
modified_key: iced::keyboard::Key::Character("Ω".into()),
physical_key: iced::keyboard::key::Physical::Code(iced::keyboard::key::Code::KeyA),
location: iced::keyboard::Location::Standard,
modifiers: iced::keyboard::Modifiers::empty(),
text: Some("Ω".into()),
repeat: false,
});
view.as_widget_mut().update(
&mut tree,
&event,
Layout::new(&node),
iced::mouse::Cursor::Unavailable,
&FontLayout,
&mut iced::advanced::clipboard::Null,
&mut Shell::new(&mut messages),
&Rectangle::new(Point::ORIGIN, size),
);
let edit = messages.pop().unwrap();
assert!(matches!(&edit, Event::Edit(id, Edit::Name(name)) if *id == token && name == "Ω"));
drop(view);
state.update(&p, scope, Id::new_v4(), edit).unwrap();
assert_eq!(
state.editing.as_ref().unwrap().kind,
None,
"Rendering/click event production must not silently adopt a kind"
);
assert_eq!(state.editing.as_ref().unwrap().name, "Ω");
assert_eq!(p, original);
}
}