From f7a228bf8f72f6aa0dea20e899c9316d0ad1eb6e Mon Sep 17 00:00:00 2001 From: Al McElrath Date: Mon, 29 Jul 2024 18:29:11 -0700 Subject: [PATCH] Remove ContentsLayout and use trait objects. - This fixes a newer compiler lifetime issue (https://github.com/rust-lang/rust/issues/42868) and requires concrete types in trait methods for object safety (`ContentsStorage` and slices for item lists). - Since item list slots can't be remapped, we need to include an offset with the context for section contents. - Section contents now requires the item list be sorted by slot and `section_items` no longer clones. - Move inline contents to a new module. --- Cargo.toml | 1 - examples/ex1.rs | 86 +++++++--------- src/contents.rs | 152 +++------------------------- src/contents/expanding.rs | 28 +++--- src/contents/grid.rs | 43 ++++---- src/contents/inline.rs | 61 ++++++++++++ src/contents/section.rs | 204 ++++++++++++++++++++++++-------------- src/lib.rs | 101 ++++++++++--------- 8 files changed, 333 insertions(+), 343 deletions(-) create mode 100644 src/contents/inline.rs diff --git a/Cargo.toml b/Cargo.toml index c5224e0..93df1f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ edition = "2021" name = "ex1" [dependencies] -ambassador = { version = "0.3.4", default-features = false } bitvec = "1.0.1" egui = "0.19.0" flagset = "0.4.3" diff --git a/examples/ex1.rs b/examples/ex1.rs index 0a28b26..81f4351 100644 --- a/examples/ex1.rs +++ b/examples/ex1.rs @@ -23,7 +23,7 @@ fn main() { } }; - let mut contents = HashMap::new(); + let mut contents = ContentsStorage::new(); let mut images = HashMap::new(); let boomerang = Item::new( @@ -64,80 +64,69 @@ fn main() { let potion2 = potion.clone().with_id(next_id()).with_name("Potion 2"); + // Setup containers. It's important to note here that there are only three containers, + // the paper doll, the ground, and the pouch. Sectioned contents is one container split + // into many sections. let paper_doll_id = next_id(); let paper_doll = SectionContents::new( SectionLayout::Grid(1), vec![ HeaderContents::new( "Bag of any! 4x4:", - GridContents::new((4, 4)) - // accepts any item - .with_flags(FlagSet::full()), + GridContents::new((4, 4)).with_flags(FlagSet::full()), // accepts any item ) - .into(), + .boxed(), HeaderContents::new( "Only potions! 2x2:", - GridContents::new((2, 2)) - // accepts only potions - .with_flags(ItemFlags::Potion), + GridContents::new((2, 2)).with_flags(ItemFlags::Potion), // accepts only potions ) - .into(), + .boxed(), HeaderContents::new( "Weapon here:", - ExpandingContents::new((2, 2)) - // accepts only weapons - .with_flags(ItemFlags::Weapon), + ExpandingContents::new((2, 2)).with_flags(ItemFlags::Weapon), // accepts only weapons ) - .into(), + .boxed(), HeaderContents::new( "Section contents 3x1x2:", SectionContents::new( SectionLayout::Grid(3), - vec![ - GridContents::new((1, 2)) - .with_flags(FlagSet::full()) // accepts any item - .into(), - GridContents::new((1, 2)) - .with_flags(FlagSet::full()) // accepts any item - .into(), - GridContents::new((1, 2)) - .with_flags(FlagSet::full()) // accepts any item - .into(), - ], + std::iter::repeat( + GridContents::new((1, 2)).with_flags(FlagSet::full()), // accepts any item + ) + .take(3) + .map(Contents::boxed) + .collect(), ), ) - .into(), + .boxed(), HeaderContents::new( "Holds a container:", InlineContents::new( - ExpandingContents::new((2, 2)) - // we only accept containers - .with_flags(ItemFlags::Container), + ExpandingContents::new((2, 2)).with_flags(ItemFlags::Container), // we only accept containers ), ) - .into(), + .boxed(), ], ); - contents.insert(paper_doll_id, (paper_doll.into(), vec![])); + + contents.insert(paper_doll_id, (paper_doll.boxed(), vec![])); let pouch_contents = SectionContents::new( SectionLayout::Grid(4), - std::iter::repeat( - GridContents::new((1, 1)) - .with_flags(ItemFlags::Potion) - .into(), - ) - .take(4) - .collect(), + std::iter::repeat(GridContents::new((1, 1)).with_flags(ItemFlags::Potion)) + .take(4) + .map(Contents::boxed) + .collect(), ); - contents.insert(pouch.id, (pouch_contents.into(), vec![])); + contents.insert(pouch.id, (Box::new(pouch_contents), vec![])); let ground_id = next_id(); let ground = GridContents::new((10, 10)).with_flags(FlagSet::full()); contents.insert( ground_id, ( - ground.into(), + Box::new(ground), + // MUST BE SORTED BY SLOT vec![ (0, boomerang), (2, pouch), @@ -151,7 +140,7 @@ fn main() { Box::new(Runic { images, drag_item: None, - contents: ContentsStorage(contents), + contents, paper_doll_id, ground_id, }) @@ -159,8 +148,6 @@ fn main() { ) } -struct ContentsStorage(HashMap)>); - //#[derive(Default)] struct Runic { #[allow(dead_code)] @@ -202,10 +189,6 @@ impl eframe::App for Runic { egui::CentralPanel::default().show(ctx, |ui| { let drag_item = &mut self.drag_item; let q = &self.contents; - let q = |id: usize| { - q.0.get(&id) - .map(|(c, i)| (c, i.iter().map(|(slot, item)| (*slot, item)))) - }; let move_data = ContainerSpace::show(drag_item, ui, |drag_item, ui| { let data = MoveData::default(); @@ -254,7 +237,6 @@ impl eframe::App for Runic { // refs would make this transactable. match self .contents - .0 .get_mut(&drag.container.0) .and_then(|(_, items)| { let idx = @@ -269,8 +251,14 @@ impl eframe::App for Runic { // Insert item. The contents must exist // already to insert an item? - match self.contents.0.get_mut(&container) { - Some((_, items)) => items.push((slot, item)), + match self.contents.get_mut(&container) { + Some((_, items)) => { + // Items must be ordered by slot in order for section contents to work. + let i = items + .binary_search_by_key(&slot, |&(slot, _)| slot) + .expect_err("item slot free"); + items.insert(i, (slot, item)); + } None => tracing::error!( "could not find container {} to add to", container diff --git a/src/contents.rs b/src/contents.rs index 3464bcb..275d885 100644 --- a/src/contents.rs +++ b/src/contents.rs @@ -1,15 +1,15 @@ use crate::*; -use ambassador::{delegatable_trait, Delegate}; pub use expanding::*; pub use grid::*; +pub use inline::*; pub use section::*; pub mod expanding; pub mod grid; +pub mod inline; pub mod section; /// A widget to display the contents of a container. -#[delegatable_trait] pub trait Contents { /// Returns an egui id based on the contents id. Unused, except /// for loading state. @@ -55,18 +55,13 @@ pub trait Contents { fn fits(&self, ctx: Context, egui_ctx: &egui::Context, item: &DragItem, slot: usize) -> bool; /// Finds the first available slot for the dragged item. - fn find_slot<'a, I>( + fn find_slot<'a>( &self, ctx: Context, egui_ctx: &egui::Context, item: &DragItem, - items: I, - ) -> Option<(usize, usize, egui::Id)> - where - I: IntoIterator, - //Q: ContentsQuery<'a>, - Self: Sized, - { + items: &[(usize, Item)], + ) -> Option<(usize, usize, egui::Id)> { find_slot_default(self, ctx, egui_ctx, item, items) } @@ -82,17 +77,13 @@ pub trait Contents { } // Draw contents. - fn body<'a, I>( + fn body( &self, _ctx: Context, _drag_item: &Option, - _items: I, + _items: &[(usize, Item)], ui: &mut egui::Ui, - ) -> egui::InnerResponse> - where - I: Iterator, - Self: Sized, - { + ) -> egui::InnerResponse> { // never used InnerResponse::new(None, ui.label("❓")) } @@ -100,22 +91,17 @@ pub trait Contents { // Default impl should handle everything including // grid/sectioned/expanding containers. Iterator type changed to // (usize, &Item) so section contents can rewrite slots. - fn ui<'a, I, Q>( + fn ui( &self, ctx: Context, - q: &'a Q, + q: &ContentsStorage, drag_item: &Option, // This used to be an option but we're generally starting with // show_contents at the root which implies items. (You can't // have items w/o a layout or vice-versa). - items: I, + items: &[(usize, Item)], ui: &mut egui::Ui, - ) -> egui::InnerResponse - where - I: IntoIterator, - Q: ContentsQuery<'a>, - Self: Sized, - { + ) -> egui::InnerResponse { assert!(match drag_item { Some(drag) => ui.memory().is_being_dragged(drag.item.eid()), _ => true, // we could be dragging something else @@ -132,8 +118,7 @@ pub trait Contents { // Reserve shape for the dragged item's shadow. let shadow = ui.painter().add(egui::Shape::Noop); - let egui::InnerResponse { inner, response } = - self.body(ctx, drag_item, items.into_iter(), &mut ui); + let egui::InnerResponse { inner, response } = self.body(ctx, drag_item, items, &mut ui); let min_rect = ui.min_rect(); @@ -142,9 +127,9 @@ pub trait Contents { match (drag_item, inner.as_ref()) { // hover ⇒ dragging (Some(drag), Some(ItemResponse::Hover((slot, item)))) => { - if let Some((contents, items)) = q.query(item.id) { + if let Some((contents, items)) = q.get(&item.id) { let ctx = item.id.into_ctx(); - let target = contents.find_slot(ctx, ui.ctx(), drag, items); + let target = contents.find_slot(ctx, ui.ctx(), drag, items.as_slice()); // TODO fits && !accepts? let color = self.shadow_color(true, target.is_some(), ui); @@ -185,7 +170,7 @@ pub trait Contents { .map(|drag| self.accepts(&drag.item)) .unwrap_or_default(); - let (id, eid) = ctx; + let (id, eid, _) = ctx; let fits = drag_item .as_ref() @@ -246,108 +231,3 @@ pub trait Contents { }) } } - -// The contents id is not relevant to the layout, just like items, -// which we removed. In particular, the ids of sections are always the -// parent container id. Maybe split Contents into two elements? -#[derive(Clone, Debug, Delegate)] -#[delegate(Contents)] -pub enum ContentsLayout { - Expanding(ExpandingContents), - Inline(InlineContents), - Grid(GridContents), - Section(SectionContents), - Header(HeaderContents), -} - -impl From for ContentsLayout { - fn from(c: ExpandingContents) -> Self { - Self::Expanding(c) - } -} - -impl From for ContentsLayout { - fn from(c: InlineContents) -> Self { - Self::Inline(c) - } -} - -impl From for ContentsLayout { - fn from(c: GridContents) -> Self { - Self::Grid(c) - } -} - -impl From for ContentsLayout { - fn from(c: SectionContents) -> Self { - Self::Section(c) - } -} - -impl From for ContentsLayout { - fn from(c: HeaderContents) -> Self { - Self::Header(c) - } -} - -// A container for a single item (or "slot") that, when containing -// another container, the interior contents are displayed inline. -#[derive(Clone, Debug)] -pub struct InlineContents(ExpandingContents); - -impl InlineContents { - pub fn new(contents: ExpandingContents) -> Self { - Self(contents) - } -} - -impl Contents for InlineContents { - fn len(&self) -> usize { - self.0.len() - } - - fn pos(&self, slot: usize) -> egui::Vec2 { - self.0.pos(slot) - } - - fn slot(&self, offset: egui::Vec2) -> usize { - self.0.slot(offset) - } - - fn accepts(&self, item: &Item) -> bool { - self.0.accepts(item) - } - - fn fits(&self, ctx: Context, egui_ctx: &egui::Context, item: &DragItem, slot: usize) -> bool { - self.0.fits(ctx, egui_ctx, item, slot) - } - - fn ui<'a, I, Q>( - &self, - ctx: Context, - q: &'a Q, - drag_item: &Option, - items: I, - ui: &mut egui::Ui, - ) -> egui::InnerResponse - where - I: IntoIterator, - Q: ContentsQuery<'a>, - { - // get the layout and contents of the contained item (if any) - let mut items = items.into_iter().peekable(); - let inline_id = items.peek().map(|(_, item)| item.id); - - // TODO: InlineLayout? - ui.horizontal(|ui| { - let data = self.0.ui(ctx, q, drag_item, items, ui).inner; - - // Don't add contents if the container is being dragged? - - match inline_id.and_then(|id| show_contents(q, id, drag_item, ui)) { - Some(resp) => data.merge(resp.inner), - None => data, - } - }) - } -} diff --git a/src/contents/expanding.rs b/src/contents/expanding.rs index d24681c..73d240e 100644 --- a/src/contents/expanding.rs +++ b/src/contents/expanding.rs @@ -54,33 +54,37 @@ impl Contents for ExpandingContents { // How do we visually show if the item is too big? What if the // item is rotated and oblong, and only fits one way? - fn fits(&self, (_id, eid): Context, ctx: &egui::Context, drag: &DragItem, slot: usize) -> bool { + fn fits( + &self, + (_id, eid, _): Context, + ctx: &egui::Context, + drag: &DragItem, + slot: usize, + ) -> bool { // Allow rotating in place. let current_item = eid == drag.container.2; let filled: bool = !current_item && ctx.data().get_temp(eid).unwrap_or_default(); slot == 0 && !filled && drag.item.shape.size.le(&self.max_size) } - fn body<'a, I>( + fn body( &self, - (id, eid): Context, + (id, eid, offset): Context, drag_item: &Option, - mut items: I, + items: &[(usize, Item)], ui: &mut egui::Ui, - ) -> egui::InnerResponse> - where - I: Iterator, - { - let item = items.next(); + ) -> egui::InnerResponse> { + let item = items.first(); ui.ctx().data().insert_temp(eid, item.is_some()); - assert!(items.next().is_none()); + assert!(items.len() <= 1); // is_rect_visible? let (new_drag, response) = match item { Some((slot, item)) => { - assert!(slot == 0); + assert_eq!(*slot, offset); + let InnerResponse { inner, response } = // item.size() isn't rotated... TODO: test // non-square containers, review item.size() everywhere @@ -89,7 +93,7 @@ impl Contents for ExpandingContents { inner.map(|item| match item { ItemResponse::NewDrag(item) => ItemResponse::Drag(DragItem { item, - container: (id, slot, eid), + container: (id, *slot, eid), cshape: None, remove_fn: None, }), diff --git a/src/contents/grid.rs b/src/contents/grid.rs index f224699..5f44c47 100644 --- a/src/contents/grid.rs +++ b/src/contents/grid.rs @@ -20,10 +20,6 @@ impl GridContents { } } -pub fn xy(slot: usize, width: usize) -> egui::Vec2 { - egui::Vec2::new((slot % width) as f32, (slot / width) as f32) -} - fn update_state( ctx: &egui::Context, id: egui::Id, @@ -63,7 +59,7 @@ impl Contents for GridContents { })) } - fn remove(&self, (_id, eid): Context, slot: usize, shape: shape::Shape) -> Option { + fn remove(&self, (_, eid, _): Context, slot: usize, shape: shape::Shape) -> Option { Some(Box::new(move |ctx, _drag, _target| { remove_shape(ctx, eid, slot, &shape) })) @@ -82,7 +78,13 @@ impl Contents for GridContents { self.flags.contains(item.flags) } - fn fits(&self, (_id, eid): Context, ctx: &egui::Context, drag: &DragItem, slot: usize) -> bool { + fn fits( + &self, + (_, eid, _): Context, + ctx: &egui::Context, + drag: &DragItem, + slot: usize, + ) -> bool { // Must be careful with the type inference here since it will // never fetch anything if it thinks it's a reference. match ctx.data().get_temp(eid) { @@ -106,24 +108,20 @@ impl Contents for GridContents { } } - fn find_slot<'a, I>( + fn find_slot( &self, ctx: Context, egui_ctx: &egui::Context, item: &DragItem, - items: I, - ) -> Option<(usize, usize, egui::Id)> - where - I: IntoIterator, - Self: Sized, - { + items: &[(usize, Item)], + ) -> Option<(usize, usize, egui::Id)> { // Prime the container shape. Normally `body` does this. let shape: Option = egui_ctx.data().get_temp(ctx.1); if shape.is_none() { let shape = items.into_iter().fold( shape::Shape::new(self.size, false), |mut shape, (slot, item)| { - shape.paint(&item.shape, slot); + shape.paint(&item.shape, *slot); shape }, ); @@ -131,26 +129,23 @@ impl Contents for GridContents { } // This will reclone the shape every turn of the loop... - find_slot_default(self, ctx, egui_ctx, item, None) + find_slot_default(self, ctx, egui_ctx, item, &[]) } - fn body<'a, I>( + fn body( &self, ctx: Context, drag_item: &Option, - items: I, + items: &[(usize, Item)], ui: &mut egui::Ui, - ) -> egui::InnerResponse> - where - I: Iterator, - { + ) -> egui::InnerResponse> { // allocate the full container size let (rect, response) = ui.allocate_exact_size( egui::Vec2::from(self.size) * ITEM_SIZE, egui::Sense::hover(), ); - let (id, eid) = ctx; + let (id, eid, offset) = ctx; let new_drag = if ui.is_rect_visible(rect) { // Skip this if the container is empty? Only if dragging into @@ -191,7 +186,10 @@ impl Contents for GridContents { let item_size = item_size(); let new_drag = items + .iter() .map(|(slot, item)| { + let slot = slot - offset; + // Paint each item and fill our shape if needed. if fill { shape.paint(&item.shape, slot); @@ -217,6 +215,7 @@ impl Contents for GridContents { // Add the contents id, current slot and // container shape w/ the item unpainted. .map(|(slot, item)| { + // let slot = slot - offset; match item { ItemResponse::NewDrag(item) => { // The dragged item shape is already rotated. We diff --git a/src/contents/inline.rs b/src/contents/inline.rs new file mode 100644 index 0000000..b2f9fe1 --- /dev/null +++ b/src/contents/inline.rs @@ -0,0 +1,61 @@ +use crate::*; + +// A container for a single item (or "slot") that, when containing +// another container, the interior contents are displayed inline. +#[derive(Clone, Debug)] +pub struct InlineContents(ExpandingContents); + +impl InlineContents { + pub fn new(contents: ExpandingContents) -> Self { + Self(contents) + } +} + +impl Contents for InlineContents { + fn len(&self) -> usize { + self.0.len() + } + + fn pos(&self, slot: usize) -> egui::Vec2 { + self.0.pos(slot) + } + + fn slot(&self, offset: egui::Vec2) -> usize { + self.0.slot(offset) + } + + fn accepts(&self, item: &Item) -> bool { + self.0.accepts(item) + } + + fn fits(&self, ctx: Context, egui_ctx: &egui::Context, item: &DragItem, slot: usize) -> bool { + self.0.fits(ctx, egui_ctx, item, slot) + } + + fn ui( + &self, + ctx: Context, + q: &ContentsStorage, + drag_item: &Option, + items: &[(usize, Item)], + ui: &mut egui::Ui, + ) -> egui::InnerResponse { + // get the layout and contents of the contained item (if any) + let inline_id = items.first().map(|(_, item)| item.id); + + // TODO: InlineLayout? + ui.horizontal(|ui| { + let data = self.0.ui(ctx, q, drag_item, items, ui).inner; + + // Don't add contents if the container is being dragged? + + match inline_id.and_then(|id| { + q.get(&id) + .map(|(contents, items)| contents.ui(id.into_ctx(), q, drag_item, items, ui)) + }) { + Some(resp) => data.merge(resp.inner), + None => data, + } + }) + } +} diff --git a/src/contents/section.rs b/src/contents/section.rs index 0f0b9a3..e7d341c 100644 --- a/src/contents/section.rs +++ b/src/contents/section.rs @@ -1,18 +1,22 @@ +use std::ops::Range; + +// use itertools::Itertools; + use crate::*; -// A sectioned container is a set of smaller containers displayed as -// one. Like pouches on a belt or different pockets in a jacket. It's -// one item than holds many fixed containers. -#[derive(Clone, Debug)] +// A sectioned container is a set of smaller containers displayed as one. Like pouches on a belt or +// different pockets in a jacket. It's one item than holds many fixed containers. It is considered +// one container, so we have to remap slots from the subcontents to the main container. pub struct SectionContents { pub layout: SectionLayout, // This should be generic over Contents but then ContentsLayout // will cycle on itself. - pub sections: Vec, + pub sections: Vec>, } #[derive(Clone)] pub enum SectionLayout { + // Number of columns... Grid(usize), // Fixed(Vec<(usize, egui::Pos2)) // Columns? @@ -31,77 +35,100 @@ impl std::fmt::Debug for SectionLayout { } impl SectionContents { - pub fn new(layout: SectionLayout, sections: Vec) -> Self { + pub fn new(layout: SectionLayout, sections: Vec>) -> Self { Self { layout, sections } } + /// Returns (section index, section slot) for `slot`. fn section_slot(&self, slot: usize) -> Option<(usize, usize)> { self.section_ranges() .enumerate() - .find_map(|(i, (start, end))| (slot < end).then(|| (i, slot - start))) + .find_map(|(i, r)| (slot < r.end).then(|| (i, slot - r.start))) } - fn section_ranges(&self) -> impl Iterator + '_ { + fn section_ranges(&self) -> impl Iterator> + '_ { let mut end = 0; self.sections.iter().map(move |s| { let start = end; end = end + s.len(); - (start, end) + start..end }) } - fn section_eid(&self, (_id, eid): Context, sid: usize) -> egui::Id { + fn section_eid(&self, (_id, eid, _): Context, sid: usize) -> egui::Id { egui::Id::new(eid.with("section").with(sid)) } // (ctx, slot) -> (section, section ctx, section slot) - fn section(&self, ctx: Context, slot: usize) -> Option<(&ContentsLayout, Context, usize)> { - self.section_slot(slot) - .map(|(i, slot)| (&self.sections[i], (ctx.0, self.section_eid(ctx, i)), slot)) + fn section(&self, ctx: Context, slot: usize) -> Option<(&dyn Contents, Context, usize)> { + self.section_slot(slot).map(|(i, slot)| { + ( + self.sections[i].as_ref(), + (ctx.0, self.section_eid(ctx, i), ctx.2), + slot, + ) + }) } - fn section_items<'a, I>( - &self, - items: I, - ) -> impl Iterator)> - where - I: IntoIterator, - { - // map (slot, item) -> (section, (slot, item)) - let ranges = self.section_ranges().collect_vec(); - - // If we know the input is sorted there is probably a way to - // do this w/o collecting into a hash map. - let mut items = items - .into_iter() - // Find section for each item. - .filter_map(|(slot, item)| { - ranges - .iter() - .enumerate() - .find_map(|(section, (start, end))| { - (slot < *end).then(|| (section, ((slot - start), item))) - }) - }) - .into_group_map(); + // TODO: enforce sortedness w/ https://github.com/rklaehn/sorted-iter or our own items collection type + fn split_items<'a>( + &'a self, + offset: usize, + mut items: &'a [(usize, Item)], + ) -> impl Iterator { + let mut ranges = self.section_ranges(); - // TODO should be a way to do this without cloning sections - self.sections - .clone() - .into_iter() - .zip(ranges.into_iter()) - .enumerate() - .map(move |(i, (layout, (start, _end)))| { - (i, layout, start, items.remove(&i).unwrap_or_default()) - }) + // This is more complicated than it needs to be, but we want to catch the assertions. + std::iter::from_fn(move || { + let Some(r) = ranges.next() else { + assert_eq!( + items.len(), + 0, + "all items inside section ranges (is `items` sorted by slot?)" + ); + return None; + }; + + // `split_once` is nightly... + let l = items + .iter() + .position(|(slot, _)| (slot - offset) >= r.end) + .unwrap_or_else(|| items.len()); + let (head, tail) = items.split_at(l); + + items = tail; + + // TODO `is_sorted` is nightly... + assert!( + head.iter().all(|(slot, _)| r.contains(&(slot - offset))), + "item slot in range (is `items` sorted by slot?)" + ); + Some((r.start, head)) + }) + } + + fn section_items<'a>( + &'a self, + offset: usize, + items: &'a [(usize, Item)], + ) -> impl Iterator { + self.split_items(offset, items) + .zip(&self.sections) + .map(|((start, items), l)| (l.as_ref(), start, items)) } } -// pub struct SectionItems { -// curr: usize, -// // keep a ref to section contents or clone sections? -// items: itertools::GroupingMap, -// } +#[allow(unused)] +fn split_lengths<'a, T>( + mut slice: &'a [T], + lens: impl IntoIterator + 'a, +) -> impl Iterator + 'a { + lens.into_iter().map(move |l| { + let (head, tail) = slice.split_at(l); + slice = tail; + head + }) +} impl Contents for SectionContents { fn len(&self) -> usize { @@ -143,49 +170,58 @@ impl Contents for SectionContents { false } - fn find_slot<'a, I>( + // Add start slot to find_slot or move slot into item. We need to modify the slot without the item reference being changed. + fn find_slot( &self, ctx: Context, egui_ctx: &egui::Context, item: &DragItem, - items: I, + items: &[(usize, Item)], // id, slot, ... - ) -> Option<(usize, usize, egui::Id)> - where - I: IntoIterator, - Self: Sized, - { - self.section_items(items) - .find_map(|(i, layout, start, items)| { - let ctx = (ctx.0, self.section_eid(ctx, i)); + ) -> Option<(usize, usize, egui::Id)> { + self.section_items(ctx.2, items) + .enumerate() + .find_map(|(i, (layout, offset, items))| { + let ctx = (ctx.0, self.section_eid(ctx, i), offset); layout .find_slot(ctx, egui_ctx, item, items) - .map(|(id, slot, eid)| (id, (slot + start), eid)) + .map(|(id, slot, eid)| (id, (slot + offset), eid)) }) } - fn ui<'a, I, Q>( + fn ui( &self, ctx: Context, - q: &'a Q, + q: &ContentsStorage, drag_item: &Option, - items: I, + items: &[(usize, Item)], ui: &mut egui::Ui, - ) -> egui::InnerResponse - where - I: IntoIterator, - Q: ContentsQuery<'a>, - Self: Sized, - { + ) -> egui::InnerResponse { let id = ctx.0; + // if !items.is_empty() { + // items + // .iter() + // .for_each(|(slot, item)| print!("[{} {} {}] ", slot, item.id, item.name)); + // println!("offset: {}", ctx.2); + // } + match self.layout { SectionLayout::Grid(width) => { egui::Grid::new(id).num_columns(width).show(ui, |ui| { - self.section_items(items) - .map(|(i, layout, start, items)| { + self.section_items(ctx.2, items) + .enumerate() + .map(|(i, (layout, offset, items))| { + //let offset = offset + ctx.2; + // dbg!(i, items.len()); let data = layout - .ui((id, self.section_eid(ctx, i)), q, drag_item, items, ui) + .ui( + (id, self.section_eid(ctx, i), offset + ctx.2), + q, + drag_item, + items, + ui, + ) .inner; if (i + 1) % width == 0 { @@ -193,9 +229,9 @@ impl Contents for SectionContents { } // Remap slots. Only if we are the subject - // of the drag or target. Nested contents + // of the drag or target. Nested containers // will have a different id. - data.map_slots(id, |slot| slot + start) + data.map_slots(id, |slot| slot + offset) }) .reduce(|acc, a| acc.merge(a)) .unwrap_or_default() @@ -205,3 +241,19 @@ impl Contents for SectionContents { } } } + +#[cfg(test)] +mod tests { + #[test] + fn split_lengths() { + let a = [1, 2, 3, 4, 5]; + + // let (x, y) = a.split_at(6.min(a.len())); + // assert_eq!(x.len(), 5); + // assert_eq!(y.len(), 0); + // assert_eq!(a.len(), 5); + + let b: Vec<_> = super::split_lengths(&a, [2usize, 3].into_iter()).collect(); + assert_eq!(b, [&vec![1, 2], &vec![3, 4, 5]]); + } +} diff --git a/src/lib.rs b/src/lib.rs index e140980..c86af69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ +use std::collections::HashMap; + pub use contents::*; use egui::{InnerResponse, TextureId}; use flagset::{flags, FlagSet}; -use itertools::Itertools; pub mod contents; pub mod shape; @@ -166,6 +167,10 @@ impl ContainerSpace { } } +pub fn xy(slot: usize, width: usize) -> egui::Vec2 { + egui::Vec2::new((slot % width) as f32, (slot / width) as f32) +} + pub fn paint_shape( idxs: Vec, shape: &shape::Shape, @@ -221,7 +226,8 @@ pub fn shape_mesh( // type Id; // } -pub type Context = (usize, egui::Id); +// Container id, egui id, item slot offset (for sectioned containers). +pub type Context = (usize, egui::Id, usize); pub trait IntoContext { fn into_ctx(self) -> Context; @@ -229,7 +235,7 @@ pub trait IntoContext { impl IntoContext for usize { fn into_ctx(self) -> Context { - (self, egui::Id::new("contents").with(self)) + (self, egui::Id::new("contents").with(self), 0) } } @@ -273,16 +279,15 @@ pub fn with_bg( InnerResponse::new(inner, response) } -pub fn find_slot_default<'a, C, I>( - contents: &C, +pub fn find_slot_default<'a, T>( + contents: &T, ctx: Context, egui_ctx: &egui::Context, drag: &DragItem, - _items: I, + _items: &[(usize, Item)], ) -> Option<(usize, usize, egui::Id)> where - C: Contents, - I: IntoIterator, + T: Contents + ?Sized, { // TODO test multiple rotations (if non-square) and return it? contents.accepts(&drag.item).then(|| true).and( @@ -310,37 +315,33 @@ flags! { /// ContentsQuery allows Contents impls to recursively query the /// contents of subcontents (InlineContents specifically). This allows /// SectionContents to use InlineContents as sections, for example. -pub trait ContentsQuery<'a> { - type Items: IntoIterator; - - fn query(&'a self, id: usize) -> Option<(&'a ContentsLayout, Self::Items)>; -} +// pub trait ContentsQuery<'a, T: Contents> { +// fn query(&self, id: usize) -> Option<(&'a T, T::Items<'a>)>; +// } // This gets around having to manually specify the iterator type when // implementing ContentsQuery. Maybe just get rid of the trait? -impl<'a, F, I> ContentsQuery<'a> for F -where - F: Fn(usize) -> Option<(&'a ContentsLayout, I)>, - I: Iterator + 'a, -{ - type Items = I; - - fn query(&'a self, id: usize) -> Option<(&'a ContentsLayout, Self::Items)> { - self(id) - } -} +// impl<'a, T, F> ContentsQuery<'a, T> for F +// where +// T: Contents + 'a, +// F: Fn(usize) -> Option<(&'a T, T::Items<'a>)>, +// // I: Iterator + 'a, +// { +// // type Items = I; + +// fn query(&self, id: usize) -> Option<(&'a T, T::Items<'a>)> { +// self(id) +// } +// } // Use ContentsQuery to query a layout and contents, then show it. -pub fn show_contents<'a, Q>( - q: &'a Q, +pub fn show_contents( + q: &ContentsStorage, id: usize, drag_item: &Option, ui: &mut egui::Ui, -) -> Option> -where - Q: ContentsQuery<'a>, -{ - q.query(id) +) -> Option> { + q.get(&id) .map(|(layout, items)| layout.ui(id.into_ctx(), q, drag_item, items, ui)) } @@ -503,7 +504,13 @@ impl Item { let p = (p - response.rect.min) / ITEM_SIZE; let slot = p.x as usize + p.y as usize * self.width(); self.shape.fill.get(slot).map(|b| *b).unwrap_or_else(|| { - tracing::error!("point {:?} slot {} out of shape fill", p, slot); + // FIX This occurs somewhere on drag/mouseover. + tracing::error!( + "point {:?} slot {} out of shape fill {}", + p, + slot, + self.shape.fill + ); false }) }) @@ -692,22 +699,27 @@ impl SlotItem for (usize, &Item) { } #[derive(Clone, Debug)] -pub struct HeaderContents { +pub struct HeaderContents { // Box ? Not clonable. pub header: String, - pub contents: Box, + pub contents: T, } -impl HeaderContents { - pub fn new(header: impl Into, contents: impl Into) -> Self { +impl HeaderContents { + pub fn new(header: impl Into, contents: T) -> Self { Self { header: header.into(), - contents: Box::new(contents.into()), + contents, } } } -impl Contents for HeaderContents { +pub type ContentsStorage = HashMap, Vec<(usize, Item)>)>; + +impl Contents for HeaderContents +where + T: Contents, +{ fn len(&self) -> usize { self.contents.len() } @@ -728,19 +740,14 @@ impl Contents for HeaderContents { self.contents.fits(ctx, egui_ctx, item, slot) } - fn ui<'a, I, Q>( + fn ui( &self, ctx: Context, - q: &'a Q, + q: &ContentsStorage, drag_item: &Option, - items: I, + items: &[(usize, Item)], ui: &mut egui::Ui, - ) -> egui::InnerResponse - where - I: IntoIterator, - Q: ContentsQuery<'a>, - Self: Sized, - { + ) -> egui::InnerResponse { // Is InnerResponse really useful? let InnerResponse { inner, response } = ui.vertical(|ui| { ui.label(&self.header);