//! A bounded catalog and focused dictionary preserve original member identities. use crate::{ app::{App, Choice, Message}, style, }; use iced::{ Element as View, Fill, widget::{button, column, container, pick_list, row, scrollable, text}, }; use oa_core::*; use std::collections::BTreeSet; #[derive(Debug, Clone, Copy)] pub enum Template { Item, Operation, PhysicalFlow, Record, Boolean, Integer, Real, Text, Enumeration, Interface, Field, } #[derive(Debug, Clone)] pub enum Event { Create(Template, Option), Allocate { interface: Id, item: Id, }, Associate { source: Id, target: Id, }, Page { section: Section, owner: Option, page: usize, }, } const PAGE_SIZE: usize = 24; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Section { Catalog, Fields, Allocations, Associations, } #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct State { catalog: usize, owner: Option, fields: usize, allocations: usize, associations: usize, } impl State { /// Browsing changes no model or property draft. pub fn update(&mut self, event: &Event) -> bool { let Event::Page { section, owner, page, } = event else { return false; }; if *section != Section::Catalog && self.owner != *owner { self.owner = *owner; self.fields = 0; self.allocations = 0; self.associations = 0; } *match section { Section::Catalog => &mut self.catalog, Section::Fields => &mut self.fields, Section::Allocations => &mut self.allocations, Section::Associations => &mut self.associations, } = *page; true } fn page(&self, section: Section, owner: Option) -> usize { if section != Section::Catalog && self.owner != owner { return 0; } match section { Section::Catalog => self.catalog, Section::Fields => self.fields, Section::Allocations => self.allocations, Section::Associations => self.associations, } } } fn page_range(total: usize, page: usize) -> std::ops::Range { let page = page.min(total.saturating_sub(1) / PAGE_SIZE); let start = page * PAGE_SIZE; start..start.saturating_add(PAGE_SIZE).min(total) } pub fn command(project: &Project, layer: Layer, event: Event) -> Result { match event { Event::Page { .. } => Err("Paging is a view action.".into()), Event::Create(template, parent) => { let (kind, name) = match template { Template::Item => (Kind::ExchangeItem, "New exchange item"), Template::Operation => (Kind::ExchangeItem, "New operation"), Template::PhysicalFlow => (Kind::ExchangeItem, "New physical flow"), Template::Interface => (Kind::Interface, "New interface"), Template::Field => (Kind::DataField, "New field"), _ => (Kind::DataType, "New data type"), }; let mut e = project .make_element(kind, layer, name, parent) .ok_or("This data kind is unavailable in the selected layer.")?; let body = match template { Template::Item | Template::Operation | Template::PhysicalFlow => { SharedBody::ExchangeItem { kind: if matches!(template, Template::PhysicalFlow) { ExchangeItemKind::Material } else { ExchangeItemKind::Information }, mechanism: match template { Template::Operation => ExchangeMechanism::Operation, Template::PhysicalFlow => ExchangeMechanism::Flow, _ => ExchangeMechanism::Unspecified, }, supertype: None, } } Template::Interface => SharedBody::Interface { supertype: None }, Template::Field => { let owner = parent .and_then(|id| project.elements.get(&id)) .ok_or("Choose the record or item that owns this field.")?; if owner.layer() != layer || !(owner.kind() == Kind::ExchangeItem || data::is_record(project, owner.id)) { return Err("Choose a record or exchange item in this layer.".into()); } let names: BTreeSet<_> = data::effective_fields(project, owner.id) .into_iter() .map(|f| f.name.as_str()) .collect(); let mut i = 1; while names.contains(e.name.as_str()) { i += 1; e.name = format!("New field {i}"); } SharedBody::DataField { type_ref: TypeRef::String, minimum: 1, maximum: Some(1), unit: String::new(), direction: if data::item_mechanism(project, owner.id) == Some(ExchangeMechanism::Operation) { ParameterDirection::In } else { ParameterDirection::Unspecified }, storage: FieldStorage::Value, default: None, } } _ => SharedBody::DataType { definition: match template { Template::Boolean => DataTypeDefinition::Boolean, Template::Integer => DataTypeDefinition::Integer { minimum: None, maximum: None, }, Template::Real => DataTypeDefinition::Real { minimum: None, maximum: None, }, Template::Text => DataTypeDefinition::String { minimum_length: 0, maximum_length: None, }, Template::Enumeration => DataTypeDefinition::Enumeration { literals: vec!["New value".into()], }, _ => DataTypeDefinition::Record { supertype: None }, }, default: None, unit: String::new(), }, }; e.body = ElementBody::shared_in(layer, body); Ok(Command::CreateElement(e)) } Event::Allocate { interface, item } => { let id = Id::new_v4(); let order = data::effective_interface_allocations(project, interface).iter().filter_map(|r| match r.body { RelationBody::InterfaceItemAllocation { order, .. } => Some(order), _ => None }).max().map_or(Ok(0), |order| order.checked_add(1).ok_or("Allocation order is full; edit existing orders before adding another allocation."))?; Ok(Command::CreateRelation(Relation { id, alias: project.fresh_alias("item_allocation", "allocation", id), name: "Interface item allocation".into(), description: String::new(), body: RelationBody::InterfaceItemAllocation { source: interface, target: item, order, transmission: TransmissionProtocol::Unspecified, acquisition: AcquisitionProtocol::Unspecified, }, })) } Event::Associate { source, target } => { let id = Id::new_v4(); Ok(Command::CreateRelation(Relation { id, alias: project.fresh_alias("data_association", "association", id), name: "Data association".into(), description: String::new(), body: RelationBody::DataAssociation { source, target, source_role: "source".into(), target_role: "target".into(), source_minimum: 0, source_maximum: None, target_minimum: 0, target_maximum: None, source_navigable: false, target_navigable: false, }, })) } } } fn action(label: &str, event: Event) -> View<'static, Message> { button(text(label.to_owned()).size(11)) .style(style::nav) .padding(7) .on_press(Message::Data(event)) .into() } fn open(label: impl Into, id: Id) -> View<'static, Message> { button(text(label.into()).size(12)) .style(style::nav) .padding(6) .on_press(Message::Select(id)) .into() } fn count(minimum: u32, maximum: Option) -> String { format!( "{minimum}..{}", maximum.map_or("*".into(), |v| v.to_string()) ) } fn type_label(p: &Project, reference: &TypeRef) -> String { match reference { TypeRef::Named(id) => p.named(*id).into(), other => format!("{other:?}"), } } fn short_label(value: &str) -> String { let mut chars = value.chars(); let mut text: String = chars.by_ref().take(120).collect(); if chars.next().is_some() { text.push('…'); } text } fn literal_label(value: &DataValue) -> String { match value { DataValue::Boolean(v) => v.to_string(), DataValue::Integer(v) => v.to_string(), DataValue::Real(v) => v.to_string(), DataValue::String(v) | DataValue::Enumeration(v) => serde_json::json!(v).to_string(), } } fn default_label(value: &Option>) -> String { match value { None => "default absent".into(), Some(v) if v.is_empty() => "explicit empty default []".into(), Some(v) => format!( "default [{}{}]", v.iter() .take(3) .map(|v| short_label(&literal_label(v))) .collect::>() .join(", "), if v.len() > 3 { ", …" } else { "" } ), } } fn definition_label(definition: &DataTypeDefinition) -> String { match definition { DataTypeDefinition::Boolean => "Boolean".into(), DataTypeDefinition::Integer { minimum, maximum } => format!( "Integer · {} … {}", minimum.map_or("−∞".into(), |v| v.to_string()), maximum.map_or("+∞".into(), |v| v.to_string()) ), DataTypeDefinition::Real { minimum, maximum } => format!( "Real · {} … {}", minimum.map_or("−∞".into(), |v| v.to_string()), maximum.map_or("+∞".into(), |v| v.to_string()) ), DataTypeDefinition::String { minimum_length, maximum_length, } => format!( "Text · {} Unicode characters", count(*minimum_length, *maximum_length) ), DataTypeDefinition::Enumeration { literals } => format!( "Enumeration · {} literals · {}", literals.len(), literals .iter() .take(4) .map(|v| short_label(v)) .collect::>() .join(" · ") ), DataTypeDefinition::Record { .. } => "Record".into(), } } fn pager( state: &State, section: Section, owner: Option, total: usize, ) -> View<'static, Message> { let range = page_range(total, state.page(section, owner)); let page = range.start / PAGE_SIZE; let mut previous = button(text("Previous").size(11)).style(style::nav); if page > 0 { previous = previous.on_press(Message::Data(Event::Page { section, owner, page: page - 1, })); } let mut next = button(text("Next").size(11)).style(style::nav); if range.end < total { next = next.on_press(Message::Data(Event::Page { section, owner, page: page + 1, })); } row![ previous, text(if total == 0 { "0 entries".into() } else { format!("{}–{} of {total}", range.start + 1, range.end) }) .size(11), next ] .spacing(8) .align_y(iced::Center) .into() } fn catalog_entries(p: &Project, layer: Layer) -> Vec<&Element> { let mut entries = p .elements .values() .filter(|e| { e.layer() == layer && matches!( e.kind(), Kind::ExchangeItem | Kind::DataType | Kind::Interface | Kind::Port ) }) .collect::>(); entries.sort_by(|a, b| { (a.kind().label(), &a.name, &a.alias).cmp(&(b.kind().label(), &b.name, &b.alias)) }); entries } fn focused_owner(p: &Project, selected: Id) -> Option<&Element> { if let Some(element) = p.elements.get(&selected) { return if element.kind() == Kind::DataField { element.parent.and_then(|id| p.elements.get(&id)) } else if matches!( element.kind(), Kind::ExchangeItem | Kind::DataType | Kind::Interface | Kind::Port ) { Some(element) } else { None }; } p.relations .get(&selected) .filter(|r| { matches!( r.kind(), RelationKind::InterfaceItemAllocation | RelationKind::DataAssociation ) }) .and_then(|r| p.elements.get(&r.source())) } fn field_row(p: &Project, owner: Id, field: &Element, selected: Id) -> View<'static, Message> { let Some(SharedBody::DataField { type_ref, minimum, maximum, direction, storage, default, .. }) = field.shared() else { return text("").into(); }; let mut content = column![open(format!("{} · @{}", field.name, field.alias), field.id)].spacing(4); let unit = data::effective_field_unit(p, field) .filter(|s| !s.is_empty()) .map(|s| format!(" · {s}")) .unwrap_or_default(); let type_default = if default.is_none() { data::effective_element_default(p, field) .map(|d| { format!( " · type default {} (per element)", short_label(&literal_label(d)) ) }) .unwrap_or_default() } else { String::new() }; content = content.push( text(format!( "{} [{}] · {storage:?} · {direction:?}{unit} · {}{type_default}", type_label(p, type_ref), count(*minimum, *maximum), default_label(default) )) .size(11) .color(style::MUTED), ); if let Some(origin) = field.parent.filter(|id| *id != owner) { content = content.push(open( format!("Inherited from {} · open original owner", p.named(origin)), origin, )); } if let TypeRef::Named(id) = type_ref { content = content.push(open( if data::is_record(p, *id) { format!( "{} · {}", if *storage == FieldStorage::Reference { "Open referenced record" } else { "Explore nested value fields" }, p.named(*id) ) } else { format!("Open named type · {}", p.named(*id)) }, *id, )); } container(content) .padding(8) .width(Fill) .style(if selected == field.id { style::card } else { style::subtle }) .into() } fn focused_details(app: &App, e: &Element) -> View<'static, Message> { let p = app.project(); let state = &app.data_state; let mut card = column![ text("Focused definition").size(16), open(format!("{} · @{}", e.name, e.alias), e.id), button(text("Edit definition").size(11)) .style(style::nav) .on_press(Message::EditProperties(e.id)) ] .spacing(8); if let Some(base) = data::supertype(e) { card = card.push(open(format!("Specializes {}", p.named(base)), base)); } match e.shared() { Some(SharedBody::ExchangeItem { kind, mechanism, .. }) => card = card.push(text(format!("{kind:?} · {mechanism:?}")).size(12)), Some(SharedBody::DataType { definition, default, unit, }) => { card = card.push( text(format!( "{}{} · {}", definition_label(definition), if unit.is_empty() { String::new() } else { format!(" · {unit}") }, default .as_ref() .map_or("default absent".into(), |d| format!( "default {}", short_label(&literal_label(d)) )) )) .size(12), ); } Some(SharedBody::Interface { .. }) => { let allocations = data::effective_interface_allocations(p, e.id); card = card.push(text("Effective item allocations").size(13)); card = card.push(pager( state, Section::Allocations, Some(e.id), allocations.len(), )); for allocation in &allocations[page_range( allocations.len(), state.page(Section::Allocations, Some(e.id)), )] { let RelationBody::InterfaceItemAllocation { target, order, transmission, acquisition, .. } = allocation.body else { continue; }; let inherited = allocation.source() != e.id; card = card.push(open( format!( "{order} · {} · {transmission:?} / {acquisition:?}{}", p.named(target), if inherited { format!(" · inherited from {}", p.named(allocation.source())) } else { String::new() } ), allocation.id, )); let mut controls = row![ open("Open item", target), button( text(if inherited { "Edit original allocation" } else { "Protocols / order" }) .size(11) ) .style(style::nav) .on_press(Message::EditProperties(allocation.id)) ] .spacing(5); if !inherited { controls = controls.push( button(text("Remove allocation").size(11)) .style(style::nav) .on_press(Message::Command(Command::Delete(allocation.id))), ); } card = card.push(controls); } let allocated = allocations .iter() .map(|r| r.target()) .collect::>(); let choices = p .elements .values() .filter(|item| { item.layer() == e.layer() && item.kind() == Kind::ExchangeItem && !allocated.contains(&item.id) }) .map(|item| Choice { id: item.id, label: format!("{} · @{}", item.name, item.alias), }) .collect::>(); let interface = e.id; card = card.push( pick_list(choices, None::, move |c| { Message::Data(Event::Allocate { interface, item: c.id, }) }) .placeholder("Allocate an existing item…") .text_size(12) .padding(7), ); } _ => {} } if e.kind() == Kind::ExchangeItem || data::is_record(p, e.id) { let fields = data::effective_fields(p, e.id); card = card.push(text("Effective fields · base fields followed by local fields").size(13)); card = card.push(pager(state, Section::Fields, Some(e.id), fields.len())); for field in &fields[page_range(fields.len(), state.page(Section::Fields, Some(e.id)))] { card = card.push(field_row(p, e.id, field, app.selected)); } card = card.push(action( if data::item_mechanism(p, e.id) == Some(ExchangeMechanism::Operation) { "+ Input parameter" } else { "+ Field" }, Event::Create(Template::Field, Some(e.id)), )); } if data::is_record(p, e.id) { let source = e.id; let choices = p .elements .values() .filter(|target| target.layer() == e.layer() && data::is_record(p, target.id)) .map(|target| Choice { id: target.id, label: format!("{} · @{}", target.name, target.alias), }) .collect::>(); card = card.push( pick_list(choices, None::, move |c| { Message::Data(Event::Associate { source, target: c.id, }) }) .placeholder("Associate with a record…") .text_size(12) .padding(7), ); let associations = p .relations .values() .filter(|r| { r.kind() == RelationKind::DataAssociation && (r.source() == e.id || r.target() == e.id) }) .collect::>(); if !associations.is_empty() { card = card.push(text("Reference associations").size(13)); card = card.push(pager( state, Section::Associations, Some(e.id), associations.len(), )); for relation in &associations[page_range( associations.len(), state.page(Section::Associations, Some(e.id)), )] { card = card.push(open( format!( "Association · {} ↔ {} · @{}", p.named(relation.source()), p.named(relation.target()), relation.alias ), relation.id, )); if let RelationBody::DataAssociation { source_navigable, target_navigable, .. } = &relation.body { card = card.push( text(format!( "{}{} · {}{}", data::association_end_label(relation, true).unwrap_or_default(), if *source_navigable { " · navigable" } else { "" }, data::association_end_label(relation, false).unwrap_or_default(), if *target_navigable { " · navigable" } else { "" } )) .size(11) .color(style::MUTED), ); } } } } container(card) .padding(12) .width(Fill) .style(style::card) .into() } pub fn view(app: &App) -> View<'_, Message> { let p = app.project(); let mut content = column![ text("Data & interfaces").size(23), text("Select a definition to explore its fields, nested types and interface contracts.") .size(12) .color(style::MUTED), row![ action("+ Item", Event::Create(Template::Item, None)), action("+ Operation", Event::Create(Template::Operation, None)), action( "+ Physical flow", Event::Create(Template::PhysicalFlow, None) ), action("+ Interface", Event::Create(Template::Interface, None)) ] .spacing(5), row![ action("+ Record", Event::Create(Template::Record, None)), action("+ Boolean", Event::Create(Template::Boolean, None)), action("+ Integer", Event::Create(Template::Integer, None)), action("+ Real", Event::Create(Template::Real, None)), action("+ Text", Event::Create(Template::Text, None)), action("+ Enumeration", Event::Create(Template::Enumeration, None)) ] .spacing(5) ] .spacing(14); if let Some(owner) = focused_owner(p, app.selected).filter(|e| e.layer() == app.layer) { content = content.push(focused_details(app, owner)); } let entries = catalog_entries(p, app.layer); content = content.push(text("Dictionary catalog").size(16)); content = content.push(pager( &app.data_state, Section::Catalog, None, entries.len(), )); for e in &entries[page_range(entries.len(), app.data_state.page(Section::Catalog, None))] { content = content.push( container( column![ open(format!("{} · @{}", e.name, e.alias), e.id), text(e.kind().label()).size(11).color(style::MUTED) ] .spacing(3), ) .padding(8) .width(Fill) .style(style::subtle), ); } scrollable(content.padding(24)).into() } #[cfg(test)] mod tests { use super::*; fn send(app: &mut App, message: Message) { let _ = app.update(message); } #[test] fn contextual_data_actions_use_typed_defaults_and_one_undo() { let mut app = App::with_project(Project::empty()); let before = app.project().clone(); send( &mut app, Message::Data(Event::Create(Template::PhysicalFlow, None)), ); let flow = app.selected; assert!( matches!( app.project().elements[&flow].shared(), Some(SharedBody::ExchangeItem { kind: ExchangeItemKind::Material, mechanism: ExchangeMechanism::Flow, .. }) ), "{}", app.status ); send(&mut app, Message::Undo); assert_eq!(app.project(), &before); send( &mut app, Message::Data(Event::Create(Template::Operation, None)), ); let operation = app.selected; send( &mut app, Message::Data(Event::Create(Template::Field, Some(operation))), ); let field = app.selected; assert_eq!(app.project().elements[&field].parent, Some(operation)); assert!( matches!( app.project().elements[&field].shared(), Some(SharedBody::DataField { minimum: 1, maximum: Some(1), direction: ParameterDirection::In, storage: FieldStorage::Value, default: None, .. }) ), "{}", app.status ); send(&mut app, Message::Undo); assert!(!app.project().contains(field)); assert!(app.project().contains(operation)); } #[test] fn allocation_rows_edit_relationship_identity_and_remove_only_that_allocation() { let mut app = App::with_project(Project::empty()); send(&mut app, Message::Data(Event::Create(Template::Item, None))); let item = app.selected; send( &mut app, Message::Data(Event::Create(Template::Interface, None)), ); let first = app.selected; send( &mut app, Message::Data(Event::Create(Template::Interface, None)), ); let second = app.selected; send( &mut app, Message::Data(Event::Allocate { interface: first, item, }), ); let allocation = app.selected; assert_eq!( app.project().relations[&allocation].source(), first, "{}", app.status ); send( &mut app, Message::Data(Event::Allocate { interface: second, item, }), ); let other = app.selected; send(&mut app, Message::EditProperties(allocation)); send( &mut app, Message::FieldShape( "/body/InterfaceItemAllocation/transmission".into(), serde_json::json!("Synchronous"), ), ); send(&mut app, Message::Apply); assert!(matches!( app.project().relations[&allocation].body, RelationBody::InterfaceItemAllocation { transmission: TransmissionProtocol::Synchronous, .. } )); assert!(matches!( app.project().relations[&other].body, RelationBody::InterfaceItemAllocation { transmission: TransmissionProtocol::Unspecified, .. } )); let before = app.project().clone(); let source_before = app.source_snapshot.clone(); send(&mut app, Message::Command(Command::Delete(allocation))); assert!(!app.project().contains(allocation)); assert!(app.project().contains(item)); assert!(app.project().contains(other)); send(&mut app, Message::Undo); assert_eq!(app.project(), &before); assert_eq!(app.source_snapshot, source_before); assert_eq!(data::interface_items(app.project(), first), vec![item]); let _ = view(&app); } #[test] fn association_creation_is_a_separate_relation_with_editable_end_contracts() { let mut app = App::with_project(Project::empty()); send( &mut app, Message::Data(Event::Create(Template::Record, None)), ); let record = app.selected; send( &mut app, Message::Data(Event::Associate { source: record, target: record, }), ); let association = app.selected; assert!( matches!(app.project().relations[&association].body,RelationBody::DataAssociation {source,target,source_navigable:false,target_navigable:false,..} if source==record&&target==record), "{}", app.status ); send(&mut app, Message::EditProperties(association)); send( &mut app, Message::Field("/body/DataAssociation/source_role".into(), "parent".into()), ); send( &mut app, Message::Field( "/body/DataAssociation/target_role".into(), "children".into(), ), ); send( &mut app, Message::FieldShape( "/body/DataAssociation/source_maximum".into(), serde_json::json!(1), ), ); send( &mut app, Message::FieldShape( "/body/DataAssociation/target_navigable".into(), serde_json::json!(true), ), ); send(&mut app, Message::Apply); assert_eq!( data::association_end_label(&app.project().relations[&association], true).as_deref(), Some("parent [0..1]") ); assert!(data::effective_fields(app.project(), record).is_empty()); } #[test] fn large_inherited_dictionary_has_bounded_widgets_and_every_original_field_is_reachable() { let mut p = Project::empty(); let mut base = None; let mut originals = vec![]; for i in 0..512 { let record = p .add_element(Kind::DataType, Layer::Oa, &format!("Record {i:03}"), None) .unwrap(); *p.elements.get_mut(&record).unwrap().shared_mut().unwrap() = SharedBody::DataType { definition: DataTypeDefinition::Record { supertype: base }, default: None, unit: String::new(), }; let field = p .add_element( Kind::DataField, Layer::Oa, &format!("Value {i}"), Some(record), ) .unwrap(); originals.push(field); base = Some(record); } let selected = base.unwrap(); let mut app = App::with_project(p); send(&mut app, Message::Select(selected)); let fields = data::effective_fields(app.project(), selected); let mut visited = vec![]; for page in 0..fields.len().div_ceil(PAGE_SIZE) { let range = page_range(fields.len(), page); assert!(range.len() <= PAGE_SIZE); visited.extend(fields[range].iter().map(|e| e.id)); } assert_eq!(visited, originals); assert_eq!( focused_owner(app.project(), originals[300]).unwrap().id, app.project().elements[&originals[300]].parent.unwrap() ); let catalog = catalog_entries(app.project(), Layer::Oa); let catalog_ids = (0..catalog.len().div_ceil(PAGE_SIZE)) .flat_map(|page| { catalog[page_range(catalog.len(), page)] .iter() .map(|e| e.id) }) .collect::>(); assert_eq!(catalog_ids.len(), 512); let view = view(&app); let tree = iced::advanced::widget::Tree::new(view.as_widget()); fn nodes(tree: &iced::advanced::widget::Tree) -> usize { 1 + tree.children.iter().map(nodes).sum::() } assert!( nodes(&tree) < 1000, "{} widget nodes for a single focused page", nodes(&tree) ); } #[test] fn browsing_pages_preserves_invalid_numeric_draft_and_inherited_allocation_identity() { let mut app = App::with_project(Project::empty()); send( &mut app, Message::Data(Event::Create(Template::Integer, None)), ); let integer = app.selected; let bound = "/body/Oa/Shared/DataType/definition/Integer/minimum"; send( &mut app, Message::FieldShape(bound.into(), serde_json::json!(0)), ); send(&mut app, Message::Field(bound.into(), "-".into())); let accepted = app.project().clone(); send( &mut app, Message::Data(Event::Page { section: Section::Catalog, owner: None, page: usize::MAX, }), ); assert_eq!(app.project(), &accepted); assert_eq!(app.selected, integer); assert_eq!(app.draft.as_ref().unwrap().inputs[bound].raw, "-"); let _ = view(&app); assert_eq!(page_range(0, usize::MAX), 0..0); let mut p = Project::empty(); let base = p .add_element(Kind::Interface, Layer::Oa, "Base contract", None) .unwrap(); let child = p .add_element(Kind::Interface, Layer::Oa, "Derived contract", None) .unwrap(); *p.elements.get_mut(&child).unwrap().shared_mut().unwrap() = SharedBody::Interface { supertype: Some(base), }; let mut originals = BTreeSet::new(); for i in 0..73 { let item = p .add_element(Kind::ExchangeItem, Layer::Oa, &format!("Item {i}"), None) .unwrap(); let allocation = p.relate( RelationKind::InterfaceItemAllocation, &format!("Allocation {i}"), base, item, ); originals.insert(allocation); } let allocations = data::effective_interface_allocations(&p, child); let visited = (0..allocations.len().div_ceil(PAGE_SIZE)) .flat_map(|page| { allocations[page_range(allocations.len(), page)] .iter() .map(|r| r.id) }) .collect::>(); assert_eq!(visited, originals); for id in visited { assert_eq!(focused_owner(&p, id).unwrap().id, base); } let mut state = State::default(); state.update(&Event::Page { section: Section::Allocations, owner: Some(child), page: 2, }); assert_eq!(state.page(Section::Allocations, Some(child)), 2); assert_eq!(state.page(Section::Allocations, Some(base)), 0); } }