1use std::fmt;
40use std::iter::Copied;
41use std::ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive};
42use std::slice;
43
44use steel_registry::item_stack::ItemStack;
45use steel_utils::locks::IntoShared;
46
47use crate::inventory::container::SimpleContainer;
48use crate::inventory::lock::{ContainerLockGuard, ContainerRef};
49use crate::inventory::menu::builder::{
50 IntoSections, MenuBuilder, MenuInstanceId, Section, SectionKind,
51};
52use crate::inventory::slots::{ResultHandler, ResultSlot, Slot};
53use crate::player::Player;
54
55const GRID_WIDTH: usize = 9;
56
57pub trait SpanBounds: sealed::Sealed {
59 #[doc(hidden)]
61 fn bounds(self) -> (usize, Option<usize>);
62}
63
64mod sealed {
65 pub trait Sealed {}
66}
67
68impl sealed::Sealed for usize {}
69impl SpanBounds for usize {
70 fn bounds(self) -> (usize, Option<usize>) {
71 (self, Some(self + 1))
72 }
73}
74
75impl sealed::Sealed for Range<usize> {}
76impl SpanBounds for Range<usize> {
77 fn bounds(self) -> (usize, Option<usize>) {
78 (self.start, Some(self.end))
79 }
80}
81
82impl sealed::Sealed for RangeInclusive<usize> {}
83impl SpanBounds for RangeInclusive<usize> {
84 fn bounds(self) -> (usize, Option<usize>) {
85 let (start, end) = self.into_inner();
86 (start, Some(end + 1))
87 }
88}
89
90impl sealed::Sealed for RangeFrom<usize> {}
91impl SpanBounds for RangeFrom<usize> {
92 fn bounds(self) -> (usize, Option<usize>) {
93 (self.start, None)
94 }
95}
96
97impl sealed::Sealed for RangeTo<usize> {}
98impl SpanBounds for RangeTo<usize> {
99 fn bounds(self) -> (usize, Option<usize>) {
100 (0, Some(self.end))
101 }
102}
103
104impl sealed::Sealed for RangeToInclusive<usize> {}
105impl SpanBounds for RangeToInclusive<usize> {
106 fn bounds(self) -> (usize, Option<usize>) {
107 (0, Some(self.end + 1))
108 }
109}
110
111impl sealed::Sealed for RangeFull {}
112impl SpanBounds for RangeFull {
113 fn bounds(self) -> (usize, Option<usize>) {
114 (0, None)
115 }
116}
117
118fn to_span(axis: &str, span: impl SpanBounds) -> (usize, Option<usize>) {
123 let (start, end) = span.bounds();
124 let len = end.map(|end| {
125 assert!(end > start, "{axis} range {start}..{end} is empty");
126 end - start
127 });
128 (start, len)
129}
130
131#[derive(Clone, Copy, PartialEq, Eq)]
148pub struct Rect {
149 x: usize,
150 y: usize,
151 w: Option<usize>,
153 h: Option<usize>,
155}
156
157impl Rect {
158 pub fn cols(cols: impl SpanBounds) -> ColSpan {
163 let (x, w) = to_span("column", cols);
164 ColSpan { x, w }
165 }
166
167 pub fn rows(rows: impl SpanBounds) -> RowSpan {
172 let (y, h) = to_span("row", rows);
173 RowSpan { y, h }
174 }
175
176 #[must_use]
178 pub const fn cell(x: usize, y: usize) -> Self {
179 Self {
180 x,
181 y,
182 w: Some(1),
183 h: Some(1),
184 }
185 }
186}
187
188impl fmt::Debug for Rect {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 fn span(f: &mut fmt::Formatter<'_>, start: usize, len: Option<usize>) -> fmt::Result {
191 match len {
192 Some(len) => write!(f, "{}..{}", start, start + len),
193 None => write!(f, "{start}.."),
194 }
195 }
196 write!(f, "Rect(cols ")?;
197 span(f, self.x, self.w)?;
198 write!(f, ", rows ")?;
199 span(f, self.y, self.h)?;
200 write!(f, ")")
201 }
202}
203
204#[derive(Clone, Copy, Debug)]
206#[must_use = "give the rect its rows to complete it"]
207pub struct ColSpan {
208 x: usize,
209 w: Option<usize>,
210}
211
212impl ColSpan {
213 #[must_use]
218 pub fn rows(self, rows: impl SpanBounds) -> Rect {
219 let (y, h) = to_span("row", rows);
220 Rect {
221 x: self.x,
222 y,
223 w: self.w,
224 h,
225 }
226 }
227}
228
229#[derive(Clone, Copy, Debug)]
231#[must_use = "give the rect its columns to complete it"]
232pub struct RowSpan {
233 y: usize,
234 h: Option<usize>,
235}
236
237impl RowSpan {
238 #[must_use]
243 pub fn cols(self, cols: impl SpanBounds) -> Rect {
244 let (x, w) = to_span("column", cols);
245 Rect {
246 x,
247 y: self.y,
248 w,
249 h: self.h,
250 }
251 }
252}
253
254#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256struct Abs {
257 x: usize,
258 y: usize,
259 w: usize,
260 h: usize,
261}
262
263impl Abs {
264 const fn area(self) -> usize {
265 self.w * self.h
266 }
267
268 const fn contains_cell(self, x: usize, y: usize) -> bool {
269 x >= self.x && x < self.x + self.w && y >= self.y && y < self.y + self.h
270 }
271
272 const fn local_index(self, x: usize, y: usize) -> usize {
274 (y - self.y) * self.w + (x - self.x)
275 }
276
277 fn cells(self) -> impl Iterator<Item = (usize, usize)> {
279 (self.y..self.y + self.h).flat_map(move |y| (self.x..self.x + self.w).map(move |x| (x, y)))
280 }
281}
282
283#[derive(Clone, Debug)]
285pub struct Region {
286 sections: Vec<Section>,
287}
288
289impl Region {
290 pub fn iter(&self) -> Copied<slice::Iter<'_, Section>> {
292 self.sections.iter().copied()
293 }
294
295 #[must_use]
297 pub fn contains(&self, slot_index: usize) -> bool {
298 self.sections.iter().any(|s| s.contains(slot_index))
299 }
300
301 #[must_use]
306 pub fn single(&self) -> Section {
307 assert!(
308 self.sections.len() == 1,
309 "region covers {} non-contiguous slot ranges; iterate sections() instead",
310 self.sections.len()
311 );
312 self.sections[0]
313 }
314}
315
316impl<'a> IntoIterator for &'a Region {
317 type Item = Section;
318 type IntoIter = Copied<slice::Iter<'a, Section>>;
319
320 fn into_iter(self) -> Self::IntoIter {
321 self.iter()
322 }
323}
324
325impl<'a> IntoSections for &'a Region {
326 type Iter = Copied<slice::Iter<'a, Section>>;
327
328 fn into_sections(self) -> Self::Iter {
329 self.iter()
330 }
331}
332
333enum Cell {
335 Empty,
337 Painted(ItemStack),
339 Functional(usize),
341}
342
343struct Placement {
345 rect: Abs,
346 kind: PlacementKind,
347}
348
349enum PlacementKind {
350 Section {
353 container: ContainerRef,
354 mapping: SlotMapping,
355 kind: SectionKind,
356 },
357 Result {
359 slot: Option<ResultSlot>,
360 container: ContainerRef,
361 },
362 Slots { slots: Vec<Option<Box<dyn Slot>>> },
364}
365
366enum SlotMapping {
368 Offset(usize),
370 Indices(Vec<usize>),
372}
373
374impl SlotMapping {
375 fn resolve(&self, local_index: usize) -> usize {
376 match self {
377 Self::Offset(offset) => offset + local_index,
378 Self::Indices(indices) => indices[local_index],
379 }
380 }
381}
382
383#[derive(Clone, Copy, PartialEq, Eq)]
384enum Axis {
385 Rows,
386 Cols,
387}
388
389struct GridState {
391 instance: MenuInstanceId,
392 base: usize,
394 width: usize,
395 cells: Vec<Cell>,
396 placements: Vec<Placement>,
397}
398
399impl GridState {
400 const fn cell_index(&self, x: usize, y: usize) -> usize {
401 y * self.width + x
402 }
403}
404
405struct Frame {
407 rect: Abs,
409 axis: Option<Axis>,
411 cursor: usize,
413 sealed: Vec<Abs>,
415}
416
417impl Frame {
418 const fn new(rect: Abs) -> Self {
419 Self {
420 rect,
421 axis: None,
422 cursor: 0,
423 sealed: Vec::new(),
424 }
425 }
426}
427
428pub struct GridPlacer<'a> {
432 state: &'a mut GridState,
433 frame: Frame,
434}
435
436impl fmt::Debug for GridPlacer<'_> {
437 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438 f.debug_struct("GridPlacer")
439 .field("width", &self.width())
440 .field("height", &self.height())
441 .finish_non_exhaustive()
442 }
443}
444
445#[must_use = "a placement does nothing until .region() or .result() is called"]
448pub struct PlacementBuilder<'p, 'a> {
449 grid: &'p mut GridPlacer<'a>,
450 rect: Rect,
451 container: ContainerRef,
452 mapping: SlotMapping,
453 kind: SectionKind,
454}
455
456impl PlacementBuilder<'_, '_> {
457 pub fn start_at(mut self, slot: usize) -> Self {
459 self.mapping = SlotMapping::Offset(slot);
460 self
461 }
462
463 pub fn at_indices(mut self, indices: impl IntoIterator<Item = usize>) -> Self {
469 self.mapping = SlotMapping::Indices(indices.into_iter().collect());
470 self
471 }
472
473 pub fn kind(mut self, kind: impl Into<SectionKind>) -> Self {
477 self.kind = kind.into();
478 self
479 }
480
481 pub fn restrict(
483 self,
484 may_place: impl Fn(usize, &ItemStack) -> bool + Send + Sync + 'static,
485 ) -> Self {
486 self.kind(SectionKind::restricted(may_place))
487 }
488
489 pub fn guard(
491 self,
492 may_place: impl Fn(usize, &ItemStack) -> bool + Send + Sync + 'static,
493 may_pickup: impl Fn(usize, &ItemStack, &ContainerLockGuard, &Player) -> bool
494 + Send
495 + Sync
496 + 'static,
497 ) -> Self {
498 self.kind(SectionKind::guarded(may_place, may_pickup))
499 }
500
501 pub fn display(self) -> Self {
503 self.kind(SectionKind::Display)
504 }
505
506 #[must_use = "hold the region to route or gate its slots"]
508 pub fn region(self) -> Region {
509 let Self {
510 grid,
511 rect,
512 container,
513 mapping,
514 kind,
515 } = self;
516 grid.place_section(rect, container, mapping, kind)
517 }
518
519 #[must_use = "hold the section to route or gate its slots"]
529 pub fn section(self) -> Section {
530 self.region().single()
531 }
532
533 pub fn result(self, handler: impl ResultHandler + 'static) -> Section {
539 let slot = ResultSlot::new(handler);
540 let result_container = slot.result_container().clone();
541 assert_eq!(
542 self.container.container_id(),
543 result_container.container_id(),
544 "result placement container must match ResultHandler::result_container"
545 );
546 self.grid.place_result(self.rect, slot, result_container)
547 }
548}
549
550impl<'a> GridPlacer<'a> {
551 #[must_use]
553 pub const fn width(&self) -> usize {
554 self.frame.rect.w
555 }
556
557 #[must_use]
559 pub const fn height(&self) -> usize {
560 self.frame.rect.h
561 }
562
563 #[must_use]
565 pub const fn full(&self) -> Rect {
566 Rect {
567 x: 0,
568 y: 0,
569 w: Some(self.frame.rect.w),
570 h: Some(self.frame.rect.h),
571 }
572 }
573
574 #[must_use]
579 pub fn size_of(&self, rect: Rect) -> (usize, usize) {
580 let abs = self.to_abs(rect);
581 (abs.w, abs.h)
582 }
583
584 pub fn place(
593 &mut self,
594 rect: Rect,
595 container: impl Into<ContainerRef>,
596 ) -> PlacementBuilder<'_, 'a> {
597 PlacementBuilder {
598 grid: self,
599 rect,
600 container: container.into(),
601 mapping: SlotMapping::Offset(0),
602 kind: SectionKind::Normal,
603 }
604 }
605
606 fn place_section(
611 &mut self,
612 rect: Rect,
613 container: ContainerRef,
614 mapping: SlotMapping,
615 kind: SectionKind,
616 ) -> Region {
617 Self::assert_mapping(&container, self.to_abs(rect), &mapping);
618 self.claim_functional(
619 rect,
620 PlacementKind::Section {
621 container,
622 mapping,
623 kind,
624 },
625 )
626 }
627
628 pub fn place_slots<S>(&mut self, rect: Rect, slots: impl IntoIterator<Item = S>) -> Region
638 where
639 S: Slot + 'static,
640 {
641 self.place_boxed_slots(
642 rect,
643 slots
644 .into_iter()
645 .map(|slot| Box::new(slot) as Box<dyn Slot>),
646 )
647 }
648
649 pub fn place_boxed_slots(
654 &mut self,
655 rect: Rect,
656 slots: impl IntoIterator<Item = Box<dyn Slot>>,
657 ) -> Region {
658 let slots: Vec<Option<Box<dyn Slot>>> = slots.into_iter().map(Some).collect();
659 let abs = self.to_abs(rect);
660 assert!(
661 slots.len() == abs.area(),
662 "place_slots got {} slots for a {}x{} rect ({} cells)",
663 slots.len(),
664 abs.w,
665 abs.h,
666 abs.area()
667 );
668 self.claim_functional(rect, PlacementKind::Slots { slots })
669 }
670
671 fn place_result(&mut self, at: Rect, slot: ResultSlot, container: ContainerRef) -> Section {
674 let abs = self.to_abs(at);
675 assert!(
676 abs.area() == 1,
677 "result placement requires a single cell, got a {}x{} rect",
678 abs.w,
679 abs.h
680 );
681 let region = self.claim_functional(
682 at,
683 PlacementKind::Result {
684 slot: Some(slot),
685 container,
686 },
687 );
688 region.single()
689 }
690
691 pub fn paint(&mut self, rect: Rect, stack: impl Into<ItemStack>) {
698 let stack = stack.into();
699 let abs = self.to_abs(rect);
700 for (x, y) in abs.cells() {
701 if self.in_sealed(x, y) {
702 continue;
703 }
704 let index = self.state.cell_index(x, y);
705 if !matches!(self.state.cells[index], Cell::Functional(_)) {
706 self.state.cells[index] = Cell::Painted(stack.clone());
707 }
708 }
709 }
710
711 pub fn paint_all(&mut self, stack: impl Into<ItemStack>) {
713 self.paint(self.full(), stack);
714 }
715
716 pub fn subgrid<R>(&mut self, rect: Rect, f: impl FnOnce(&mut GridPlacer<'_>) -> R) -> R {
723 let abs = self.to_abs(rect);
724 for (x, y) in abs.cells() {
725 assert!(
726 !self.in_sealed(x, y),
727 "subgrid {rect:?} overlaps another subgrid at local cell ({}, {})",
728 x - self.frame.rect.x,
729 y - self.frame.rect.y
730 );
731 let index = self.state.cell_index(x, y);
732 match self.state.cells[index] {
733 Cell::Functional(_) => panic!(
734 "subgrid {rect:?} overlaps a placement at local cell ({}, {})",
735 x - self.frame.rect.x,
736 y - self.frame.rect.y
737 ),
738 Cell::Painted(_) => self.state.cells[index] = Cell::Empty,
740 Cell::Empty => {}
741 }
742 }
743
744 let mut child = GridPlacer {
745 state: &mut *self.state,
746 frame: Frame::new(abs),
747 };
748 let result = f(&mut child);
749 child.check_coverage();
750 self.frame.sealed.push(abs);
751 result
752 }
753
754 pub fn carve_rows<R>(&mut self, count: usize, f: impl FnOnce(&mut GridPlacer<'_>) -> R) -> R {
759 self.carve(Axis::Rows, count, f)
760 }
761
762 pub fn carve_cols<R>(&mut self, count: usize, f: impl FnOnce(&mut GridPlacer<'_>) -> R) -> R {
767 self.carve(Axis::Cols, count, f)
768 }
769
770 pub fn rest<R>(&mut self, f: impl FnOnce(&mut GridPlacer<'_>) -> R) -> R {
775 let axis = self.frame.axis.unwrap_or(Axis::Rows);
776 let remaining = match axis {
777 Axis::Rows => self.height() - self.frame.cursor,
778 Axis::Cols => self.width() - self.frame.cursor,
779 };
780 assert!(
781 remaining > 0,
782 "rest() called with nothing remaining to carve"
783 );
784 self.carve(axis, remaining, f)
785 }
786
787 fn carve<R>(
788 &mut self,
789 axis: Axis,
790 count: usize,
791 f: impl FnOnce(&mut GridPlacer<'_>) -> R,
792 ) -> R {
793 assert!(count > 0, "cannot carve zero rows/columns");
794 assert!(
795 self.frame.axis.is_none_or(|a| a == axis),
796 "cannot mix rows() and cols() in one grid scope; open a subgrid to switch axes"
797 );
798 let (remaining, local) = match axis {
799 Axis::Rows => (
800 self.height() - self.frame.cursor,
801 Rect {
802 x: 0,
803 y: self.frame.cursor,
804 w: None,
805 h: Some(count),
806 },
807 ),
808 Axis::Cols => (
809 self.width() - self.frame.cursor,
810 Rect {
811 x: self.frame.cursor,
812 y: 0,
813 w: Some(count),
814 h: None,
815 },
816 ),
817 };
818 assert!(
819 count <= remaining,
820 "carving {count} {} exceeds the {remaining} remaining",
821 match axis {
822 Axis::Rows => "rows",
823 Axis::Cols => "columns",
824 }
825 );
826 self.frame.axis = Some(axis);
827 self.frame.cursor += count;
828 self.subgrid(local, f)
829 }
830
831 fn to_abs(&self, rect: Rect) -> Abs {
836 let frame = self.frame.rect;
837 let w = rect.w.unwrap_or_else(|| frame.w.saturating_sub(rect.x));
838 let h = rect.h.unwrap_or_else(|| frame.h.saturating_sub(rect.y));
839 assert!(
840 w > 0 && h > 0 && rect.x + w <= frame.w && rect.y + h <= frame.h,
841 "rect {rect:?} exceeds the {}x{} grid area",
842 frame.w,
843 frame.h
844 );
845 Abs {
846 x: frame.x + rect.x,
847 y: frame.y + rect.y,
848 w,
849 h,
850 }
851 }
852
853 fn in_sealed(&self, x: usize, y: usize) -> bool {
855 self.frame.sealed.iter().any(|r| r.contains_cell(x, y))
856 }
857
858 fn claim_functional(&mut self, rect: Rect, kind: PlacementKind) -> Region {
860 let abs = self.to_abs(rect);
861 for (x, y) in abs.cells() {
862 assert!(
863 !self.in_sealed(x, y),
864 "rect {rect:?} overlaps a subgrid at local cell ({}, {})",
865 x - self.frame.rect.x,
866 y - self.frame.rect.y
867 );
868 assert!(
869 !matches!(
870 self.state.cells[self.state.cell_index(x, y)],
871 Cell::Functional(_)
872 ),
873 "rect {rect:?} overlaps another placement at local cell ({}, {})",
874 x - self.frame.rect.x,
875 y - self.frame.rect.y
876 );
877 }
878
879 let placement = self.state.placements.len();
880 for (x, y) in abs.cells() {
881 let index = self.state.cell_index(x, y);
882 self.state.cells[index] = Cell::Functional(placement);
883 }
884 self.state.placements.push(Placement { rect: abs, kind });
885 self.region_for(abs)
886 }
887
888 fn region_for(&self, abs: Abs) -> Region {
890 let mut sections: Vec<(usize, usize)> = Vec::new();
891 for y in abs.y..abs.y + abs.h {
892 let start = self.state.base + y * self.state.width + abs.x;
893 match sections.last_mut() {
894 Some(last) if last.1 == start => last.1 = start + abs.w,
895 _ => sections.push((start, start + abs.w)),
896 }
897 }
898 Region {
899 sections: sections
900 .into_iter()
901 .map(|(start, end)| Section::new(self.state.instance, start..end))
902 .collect(),
903 }
904 }
905
906 fn check_coverage(&self) {
908 let holes: Vec<(usize, usize)> = self
909 .frame
910 .rect
911 .cells()
912 .filter(|&(x, y)| matches!(self.state.cells[self.state.cell_index(x, y)], Cell::Empty))
913 .map(|(x, y)| (x - self.frame.rect.x, y - self.frame.rect.y))
914 .collect();
915 assert!(
916 holes.is_empty(),
917 "grid area not fully covered; place or paint the local cells (column, row): {holes:?}"
918 );
919 }
920
921 fn assert_mapping(container: &ContainerRef, rect: Abs, mapping: &SlotMapping) {
923 use crate::inventory::lock::ContainerLockGuard;
924
925 let size = ContainerLockGuard::lock_all(slice::from_ref(container))
926 .get(container.container_id())
927 .expect("container was just locked")
928 .get_container_size();
929 match mapping {
930 SlotMapping::Offset(offset) => assert!(
931 offset + rect.area() <= size,
932 "placement needs container slots {}..{}, but the container only has {size} slots",
933 offset,
934 offset + rect.area()
935 ),
936 SlotMapping::Indices(indices) => {
937 assert!(
938 indices.len() == rect.area(),
939 "at_indices got {} slots for a {}x{} rect ({} cells)",
940 indices.len(),
941 rect.w,
942 rect.h,
943 rect.area()
944 );
945 for &index in indices {
946 assert!(
947 index < size,
948 "at_indices maps to container slot {index}, but the container only has {size} slots"
949 );
950 }
951 }
952 }
953 }
954}
955
956impl MenuBuilder {
957 pub fn grid<R>(&mut self, rows: usize, f: impl FnOnce(&mut GridPlacer<'_>) -> R) -> R {
965 assert!(rows > 0, "grid needs at least one row");
966 assert!(
967 self.slot_count().is_multiple_of(GRID_WIDTH),
968 "grid starts mid-row (slot {}); previous sections must fill complete rows of {GRID_WIDTH}",
969 self.slot_count()
970 );
971
972 let mut state = GridState {
973 instance: self.instance(),
974 base: self.slot_count(),
975 width: GRID_WIDTH,
976 cells: (0..GRID_WIDTH * rows).map(|_| Cell::Empty).collect(),
977 placements: Vec::new(),
978 };
979 let mut placer = GridPlacer {
980 state: &mut state,
981 frame: Frame::new(Abs {
982 x: 0,
983 y: 0,
984 w: GRID_WIDTH,
985 h: rows,
986 }),
987 };
988 let result = f(&mut placer);
989 placer.check_coverage();
990 self.flush_grid(state);
991 result
992 }
993
994 fn flush_grid(&mut self, state: GridState) {
996 let GridState {
997 cells,
998 mut placements,
999 width,
1000 ..
1001 } = state;
1002 for placement in &placements {
1003 match &placement.kind {
1004 PlacementKind::Section {
1005 container, mapping, ..
1006 } => match mapping {
1007 SlotMapping::Offset(offset) => {
1008 self.claim(container, (*offset..offset + placement.rect.area()).into());
1009 }
1010 SlotMapping::Indices(indices) => {
1011 for &index in indices {
1012 self.claim(container, (index..index + 1).into());
1013 }
1014 }
1015 },
1016 PlacementKind::Result { container, .. } => {
1017 self.claim(container, (0..1).into());
1018 }
1019 PlacementKind::Slots { .. } => {}
1020 }
1021 }
1022
1023 let painted: Vec<ItemStack> = cells
1024 .iter()
1025 .filter_map(|cell| match cell {
1026 Cell::Painted(stack) => Some(stack.clone()),
1027 _ => None,
1028 })
1029 .collect();
1030 let filler = (!painted.is_empty())
1031 .then(|| ContainerRef::from(SimpleContainer::from_items(painted).into_shared()));
1032
1033 let mut filler_next = 0;
1034 for (index, cell) in cells.iter().enumerate() {
1035 let (x, y) = (index % width, index / width);
1036 match cell {
1037 Cell::Empty => unreachable!("coverage was checked before flushing"),
1038 Cell::Painted(_) => {
1039 let container = filler
1040 .as_ref()
1041 .expect("filler exists when cells are painted");
1042 let slot = SectionKind::Display.make(container, filler_next);
1043 self.push_section_slot(slot, container, filler_next);
1044 filler_next += 1;
1045 }
1046 Cell::Functional(placement) => {
1047 let Placement { rect, kind } = &mut placements[*placement];
1048 match kind {
1049 PlacementKind::Section {
1050 container,
1051 mapping,
1052 kind,
1053 } => {
1054 let container_index = mapping.resolve(rect.local_index(x, y));
1055 let slot = kind.make(container, container_index);
1056 self.push_section_slot(slot, container, container_index);
1057 }
1058 PlacementKind::Result { slot, container } => {
1059 let slot = slot
1060 .take()
1061 .expect("each result placement maps to exactly one slot");
1062 self.push_section_slot(Box::new(slot), container, 0);
1063 }
1064 PlacementKind::Slots { slots, .. } => {
1065 let slot = slots[rect.local_index(x, y)]
1066 .take()
1067 .expect("each grid cell maps to exactly one slot");
1068 self.push_boxed_slot(slot);
1069 }
1070 }
1071 }
1072 }
1073 }
1074 }
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use super::*;
1080 use crate::inventory::{
1081 lock::ContainerLockGuard,
1082 slots::{NormalSlot, ResultHandler},
1083 };
1084 use crate::player::Player;
1085 use steel_utils::locks::IntoShared;
1086
1087 struct NoopResultHandler(ContainerRef);
1088
1089 impl ResultHandler for NoopResultHandler {
1090 fn result_container(&self) -> ContainerRef {
1091 self.0.clone()
1092 }
1093
1094 fn dependencies(&self) -> Vec<ContainerRef> {
1095 Vec::new()
1096 }
1097
1098 fn update_result(&self, _guard: &mut ContainerLockGuard) {}
1099
1100 fn on_result_taken(
1101 &self,
1102 _guard: &mut ContainerLockGuard,
1103 _player: &Player,
1104 ) -> Option<ItemStack> {
1105 None
1106 }
1107
1108 fn is_result_valid(&self, _guard: &ContainerLockGuard, _player: &Player) -> bool {
1109 true
1110 }
1111 }
1112
1113 fn container(size: usize) -> ContainerRef {
1114 ContainerRef::from(SimpleContainer::new(size).into_shared())
1115 }
1116
1117 fn ranges(region: &Region) -> Vec<(usize, usize)> {
1118 region.iter().map(|s| (s.start(), s.end())).collect()
1119 }
1120
1121 #[test]
1122 fn full_width_placement_merges_into_one_section() {
1123 let mut b = MenuBuilder::new(None, 0);
1124 let region = b.grid(2, |g| g.place(g.full(), container(18)).region());
1125 assert_eq!(ranges(®ion), vec![(0, 18)]);
1126 assert_eq!(b.slot_count(), 18);
1127 }
1128
1129 #[test]
1130 fn narrow_placement_yields_one_section_per_row() {
1131 let mut b = MenuBuilder::new(None, 0);
1132 let region = b.grid(3, |g| {
1133 let region = g.place(Rect::cols(1..4).rows(..), container(9)).region();
1134 g.paint_all(ItemStack::empty());
1135 region
1136 });
1137 assert_eq!(ranges(®ion), vec![(1, 4), (10, 13), (19, 22)]);
1138 assert_eq!(b.slot_count(), 27);
1139 }
1140
1141 #[test]
1142 fn place_slots_lands_in_row_major_order() {
1143 use crate::inventory::menu::kinds::BasicKind;
1144
1145 let c = container(9);
1146 let slots: Vec<NormalSlot> = (3..7).map(|i| NormalSlot::new(c.clone(), i)).collect();
1147
1148 let mut b = MenuBuilder::new(None, 0);
1149 let region = b.grid(1, |g| {
1150 let region = g.place_slots(Rect::cols(0..4).rows(..), slots);
1151 g.paint_all(ItemStack::empty());
1152 region
1153 });
1154
1155 assert_eq!(ranges(®ion), vec![(0, 4)]);
1156 assert_eq!(b.slot_count(), 9);
1157
1158 let menu = b.build(BasicKind);
1159 let keys: Vec<usize> = (0..4)
1160 .map(|menu_slot| {
1161 menu.behavior().slots()[menu_slot]
1162 .storage()
1163 .physical_key()
1164 .expect("place_slots slots are container-backed")
1165 .1
1166 })
1167 .collect();
1168 assert_eq!(keys, vec![3, 4, 5, 6]);
1169 }
1170
1171 #[test]
1172 fn at_indices_maps_cells_in_the_given_order() {
1173 use crate::inventory::menu::kinds::BasicKind;
1174
1175 let mut b = MenuBuilder::new(None, 0);
1176 let region = b.grid(1, |g| {
1177 let region = g
1178 .place(Rect::cols(0..4).rows(..), container(9))
1179 .at_indices([8, 6, 4, 2])
1180 .region();
1181 g.paint_all(ItemStack::empty());
1182 region
1183 });
1184
1185 assert_eq!(ranges(®ion), vec![(0, 4)]);
1186
1187 let menu = b.build(BasicKind);
1188 let keys: Vec<usize> = (0..4)
1189 .map(|menu_slot| {
1190 menu.behavior().slots()[menu_slot]
1191 .storage()
1192 .physical_key()
1193 .expect("at_indices slots are container-backed")
1194 .1
1195 })
1196 .collect();
1197 assert_eq!(keys, vec![8, 6, 4, 2]);
1198 }
1199
1200 #[test]
1201 #[should_panic(expected = "at_indices got 3 slots for a 4x1 rect (4 cells)")]
1202 fn at_indices_rejects_a_count_that_disagrees_with_the_rect() {
1203 let mut b = MenuBuilder::new(None, 0);
1204 b.grid(1, |g| {
1205 let _ = g
1206 .place(Rect::cols(0..4).rows(..), container(9))
1207 .at_indices([0, 1, 2])
1208 .region();
1209 });
1210 }
1211
1212 #[test]
1213 fn kind_lowers_cells_through_a_custom_factory() {
1214 use crate::inventory::menu::kinds::BasicKind;
1215
1216 let factory = SectionKind::custom(|container, index| {
1217 Box::new(NormalSlot::new(container.clone(), index))
1218 });
1219
1220 let mut b = MenuBuilder::new(None, 0);
1221 b.grid(1, |g| {
1222 let _ = g
1223 .place(Rect::cols(0..2).rows(..), container(2))
1224 .kind(factory)
1225 .region();
1226 g.paint_all(ItemStack::empty());
1227 });
1228
1229 let menu = b.build(BasicKind);
1230 let keys: Vec<usize> = (0..2)
1231 .map(|menu_slot| {
1232 menu.behavior().slots()[menu_slot]
1233 .storage()
1234 .physical_key()
1235 .expect("custom factory slots are container-backed")
1236 .1
1237 })
1238 .collect();
1239 assert_eq!(keys, vec![0, 1]);
1240 }
1241
1242 #[test]
1243 #[should_panic(expected = "place_slots got 3 slots")]
1244 fn place_slots_panics_on_count_mismatch() {
1245 let c = container(9);
1246 let slots: Vec<NormalSlot> = (0..3).map(|i| NormalSlot::new(c.clone(), i)).collect();
1247
1248 let mut b = MenuBuilder::new(None, 0);
1249 b.grid(1, |g| {
1250 g.place_slots(Rect::cols(0..4).rows(..), slots);
1251 g.paint_all(ItemStack::empty());
1252 });
1253 }
1254
1255 #[test]
1256 fn sibling_grids_stack_vertically() {
1257 let mut b = MenuBuilder::new(None, 0);
1258 let top = b.grid(1, |g| g.place(g.full(), container(9)).region());
1259 let bottom = b.grid(1, |g| g.place(g.full(), container(9)).region());
1260 assert_eq!(ranges(&top), vec![(0, 9)]);
1261 assert_eq!(ranges(&bottom), vec![(9, 18)]);
1262 }
1263
1264 #[test]
1265 fn cols_carve_side_by_side() {
1266 let mut b = MenuBuilder::new(None, 0);
1267 let (left, mid, right) = b.grid(2, |g| {
1268 let left = g.carve_cols(4, |g| g.place(g.full(), container(8)).region());
1269 let mid = g.carve_cols(1, |g| g.place(g.full(), container(2)).region());
1270 let right = g.rest(|g| g.place(g.full(), container(8)).region());
1271 (left, mid, right)
1272 });
1273 assert_eq!(ranges(&left), vec![(0, 4), (9, 13)]);
1274 assert_eq!(ranges(&mid), vec![(4, 5), (13, 14)]);
1275 assert_eq!(ranges(&right), vec![(5, 9), (14, 18)]);
1276 }
1277
1278 #[test]
1279 fn rows_and_offset_carve_one_container() {
1280 let mut b = MenuBuilder::new(None, 0);
1281 let shared = container(54);
1282 let (top, body) = b.grid(6, |g| {
1283 let top = g.carve_rows(1, |g| g.place(g.full(), shared.clone()).region());
1284 let body = g.rest(|g| g.place(g.full(), shared.clone()).start_at(9).region());
1285 (top.single(), body.single())
1286 });
1287 assert_eq!((top.start(), top.end()), (0, 9));
1288 assert_eq!((body.start(), body.end()), (9, 54));
1289 }
1290
1291 #[test]
1292 fn restricted_placement_covers_like_place() {
1293 let mut b = MenuBuilder::new(None, 0);
1294 let region = b.grid(2, |g| {
1295 let region = g
1296 .place(Rect::cols(2..5).rows(..), container(6))
1297 .guard(|_slot, _stack| true, |_, _, _, _| false)
1298 .region();
1299 g.paint_all(ItemStack::empty());
1300 region
1301 });
1302 assert_eq!(ranges(®ion), vec![(2, 5), (11, 14)]);
1303 assert_eq!(b.slot_count(), 18);
1304 }
1305
1306 #[test]
1307 fn placements_mask_paint_in_any_order() {
1308 let mut b = MenuBuilder::new(None, 0);
1309 b.grid(2, |g| {
1310 g.paint_all(ItemStack::empty());
1311 let _ = g.place(Rect::cols(0..2).rows(0), container(2)).region();
1312 let _ = g.place(Rect::cols(2..4).rows(0), container(2)).region();
1313 });
1314 assert_eq!(b.slot_count(), 18);
1315 }
1316
1317 #[test]
1318 fn result_slot_lands_on_its_cell() {
1319 use crate::inventory::container::ResultContainer;
1320
1321 let container = ContainerRef::from(ResultContainer::new().into_shared());
1322 let mut b = MenuBuilder::new(None, 0);
1323 let result = b.grid(3, |g| {
1324 let result = g
1325 .place(Rect::cell(6, 2), container.clone())
1326 .result(NoopResultHandler(container.clone()));
1327 g.paint_all(ItemStack::empty());
1328 result
1329 });
1330 assert_eq!((result.start(), result.end()), (24, 25));
1331 }
1332
1333 #[test]
1334 #[should_panic(
1335 expected = "result placement container must match ResultHandler::result_container"
1336 )]
1337 fn result_placement_rejects_a_container_that_differs_from_the_handler() {
1338 let placed = container(1);
1339 let handled = container(1);
1340 let mut b = MenuBuilder::new(None, 0);
1341
1342 b.grid(1, |g| {
1343 let _ = g
1344 .place(Rect::cell(0, 0), placed)
1345 .result(NoopResultHandler(handled));
1346 });
1347 }
1348
1349 #[test]
1350 #[should_panic(
1351 expected = "section takes container slots 0..1, but the container only has 0 slots"
1352 )]
1353 fn result_placement_rejects_a_container_without_slot_zero() {
1354 let container = container(0);
1355 let mut b = MenuBuilder::new(None, 0);
1356
1357 b.grid(1, |g| {
1358 let _ = g
1359 .place(Rect::cell(0, 0), container.clone())
1360 .result(NoopResultHandler(container.clone()));
1361 g.paint_all(ItemStack::empty());
1362 });
1363 }
1364
1365 #[test]
1366 #[should_panic(expected = "two sections cover overlapping slots")]
1367 fn result_placement_rejects_a_normal_alias() {
1368 let container = container(1);
1369 let mut b = MenuBuilder::new(None, 0);
1370
1371 b.grid(1, |g| {
1372 let _ = g.place(Rect::cell(0, 0), container.clone()).section();
1373 let _ = g
1374 .place(Rect::cell(1, 0), container.clone())
1375 .result(NoopResultHandler(container.clone()));
1376 g.paint_all(ItemStack::empty());
1377 });
1378 }
1379
1380 #[test]
1381 #[should_panic(expected = "fake slots require exclusive backing storage")]
1382 fn raw_grid_slots_cannot_alias_result_backing_storage() {
1383 use crate::inventory::menu::kinds::BasicKind;
1384
1385 let container = container(1);
1386 let slots: Vec<Box<dyn Slot>> = vec![
1387 Box::new(NormalSlot::new(container.clone(), 0)),
1388 Box::new(ResultSlot::new(NoopResultHandler(container.clone()))),
1389 ];
1390 let mut b = MenuBuilder::new(None, 0);
1391 b.grid(1, |g| {
1392 let _ = g.place_boxed_slots(Rect::cols(0..2).rows(0), slots);
1393 g.paint_all(ItemStack::empty());
1394 });
1395
1396 let _ = b.build(BasicKind);
1397 }
1398
1399 #[test]
1400 #[should_panic(expected = "overlaps another placement")]
1401 fn overlapping_placements_panic() {
1402 let mut b = MenuBuilder::new(None, 0);
1403 b.grid(1, |g| {
1404 let _ = g.place(Rect::cols(0..5).rows(0), container(5)).region();
1405 let _ = g.place(Rect::cols(4..9).rows(0), container(5)).region();
1406 });
1407 }
1408
1409 #[test]
1410 #[should_panic(expected = "exceeds the 9x1 grid area")]
1411 fn out_of_bounds_placement_panics() {
1412 let mut b = MenuBuilder::new(None, 0);
1413 b.grid(1, |g| {
1414 let _ = g.place(Rect::cols(5..10).rows(0), container(5)).region();
1415 });
1416 }
1417
1418 #[test]
1419 #[should_panic(expected = "not fully covered")]
1420 fn uncovered_cells_panic() {
1421 let mut b = MenuBuilder::new(None, 0);
1422 b.grid(1, |g| {
1423 let _ = g.place(Rect::cols(0..4).rows(0), container(4)).region();
1424 });
1425 }
1426
1427 #[test]
1428 #[should_panic(expected = "not fully covered")]
1429 fn subgrid_must_cover_itself_despite_parent_paint() {
1430 let mut b = MenuBuilder::new(None, 0);
1431 b.grid(2, |g| {
1432 g.paint_all(ItemStack::empty());
1433 g.subgrid(Rect::cols(0..4).rows(0), |g| {
1434 let _ = g.place(Rect::cols(0..2).rows(0), container(2)).region();
1435 });
1436 });
1437 }
1438
1439 #[test]
1440 #[should_panic(expected = "cannot mix rows() and cols()")]
1441 fn mixing_carve_axes_panics() {
1442 let mut b = MenuBuilder::new(None, 0);
1443 b.grid(2, |g| {
1444 g.carve_rows(1, |g| g.place(g.full(), container(9)).region());
1445 g.carve_cols(4, |g| g.place(g.full(), container(4)).region());
1446 });
1447 }
1448
1449 #[test]
1450 #[should_panic(expected = "grid starts mid-row")]
1451 fn grid_after_partial_row_panics() {
1452 let mut b = MenuBuilder::new(None, 0);
1453 b.section(container(5), 5);
1454 b.grid(1, |g| {
1455 g.paint_all(ItemStack::empty());
1456 });
1457 }
1458
1459 #[test]
1460 fn range_flavors_and_axis_orders_agree() {
1461 let mut b = MenuBuilder::new(None, 0);
1462 let (left, right) = b.grid(2, |g| {
1463 let left = g.place(Rect::cols(..=3).rows(..), container(8)).region();
1464 let right = g.place(Rect::rows(..).cols(4..), container(10)).region();
1465 (left, right)
1466 });
1467 assert_eq!(ranges(&left), vec![(0, 4), (9, 13)]);
1468 assert_eq!(ranges(&right), vec![(4, 9), (13, 18)]);
1469 }
1470
1471 #[test]
1472 fn unbounded_ends_resolve_against_the_subgrid() {
1473 let mut b = MenuBuilder::new(None, 0);
1474 let inner = b.grid(2, |g| {
1475 g.paint_all(ItemStack::empty());
1476 g.subgrid(Rect::cols(1..5).rows(0), |g| {
1477 let inner = g.place(Rect::cols(2..).rows(..), container(2)).region();
1478 g.paint_all(ItemStack::empty());
1479 inner
1480 })
1481 });
1482 assert_eq!(ranges(&inner), vec![(3, 5)]);
1483 }
1484
1485 #[test]
1486 #[should_panic(expected = "column range 3..3 is empty")]
1487 fn empty_range_panics_at_construction() {
1488 let _ = Rect::cols(3..3);
1489 }
1490
1491 #[test]
1492 #[should_panic(expected = "exceeds the 9x1 grid area")]
1493 fn from_range_starting_past_the_edge_panics() {
1494 let mut b = MenuBuilder::new(None, 0);
1495 b.grid(1, |g| {
1496 let _ = g.place(Rect::cols(9..).rows(..), container(1)).region();
1497 });
1498 }
1499
1500 #[test]
1501 #[should_panic(expected = "non-contiguous")]
1502 fn single_panics_on_multi_row_narrow_region() {
1503 let mut b = MenuBuilder::new(None, 0);
1504 b.grid(2, |g| {
1505 let region = g.place(Rect::cols(0..4).rows(..), container(8)).region();
1506 g.paint_all(ItemStack::empty());
1507 let _ = region.single();
1508 });
1509 }
1510}