Skip to main content

steel_core/inventory/menu/
grid.rs

1//! Builds sections by placing rectangles on a canvas instead of placing sections by hand.
2//!
3//! Only works with row-major menus, not ones with protocol-defined slot indices like anvils.
4//! Those still build through [`MenuBuilder`]'s normal placements.
5//!
6//! ```rust
7//! use steel_registry::{vanilla_items, vanilla_menu_types};
8//! use steel_core::inventory::menu::kinds::BasicKind;
9//! use steel_core::player::player_inventory::PlayerInventory;
10//!
11//! use steel_core::inventory::prelude::*;
12//!
13//! fn example(container_id: u8, inventory: Shared<PlayerInventory>) -> Menu {
14//!     let mut b = MenuBuilder::new(&vanilla_menu_types::GENERIC_9X3, container_id);
15//!
16//!     let storage = SimpleContainer::new(3).into_shared();
17//!
18//!     let items = b.grid(3, |g| {
19//!         let items = g.place(Rect::cols(3..6).rows(1), storage).section();
20//!         g.paint_all(&vanilla_items::GRAY_STAINED_GLASS_PANE);
21//!         items
22//!     });
23//!
24//!     let player = b.player_inventory(&inventory);
25//!     b.route(items, player.all(), FillDirection::Backward);
26//!     b.route(player.all(), items, FillDirection::Forward);
27//!     b.build(BasicKind)
28//! }
29//! ```
30//!
31//! # Rules
32//!
33//! - Placements never overlap. A cell belongs to at most one placement. A second claim panics.
34//! - Paint is decoration. It layers freely (last paint wins) and placements always mask it. Painted cells become locked display slots of one auto-sized filler container.
35//! - Every cell must be placed or painted when a scope closes, else panic.
36//! - Subgrids are self-contained. [`GridPlacer::subgrid`] has its own local coordinates and coverage check. Parent paint does not reach into it.
37//! - [`GridPlacer::carve_rows`], [`GridPlacer::carve_cols`] and [`GridPlacer::rest`] are cursor-computed subgrids. One carve axis per scope. Nest to switch axes.
38
39use 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
57/// A column or row selection for [`Rect::cols`] / [`Rect::rows`]: any range or a bare index.
58pub trait SpanBounds: sealed::Sealed {
59    /// Lowers to `(start, exclusive end)`. A `None` end means the scope's edge.
60    #[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
118/// Lowers a [`SpanBounds`] to `(start, length)`. A `None` length means the scope's edge.
119///
120/// # Panics
121/// If the range is empty.
122fn 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/// A rectangle of grid cells, selected by column and row ranges. Coordinates
132/// are 0-based from the top-left of the scope the rect is used in.
133///
134/// Built by giving both axes, in either order. See [`SpanBounds`] for the accepted range forms.
135///
136/// ```rust
137/// use steel_core::inventory::menu::Rect;
138///
139/// Rect::cols(3..6).rows(1);      // columns 3,4,5 of row 1
140/// Rect::rows(1..=2).cols(..4);   // the same rect, axes given in the other order
141/// Rect::cols(4..).rows(..);      // column 4 to the right edge, all rows
142/// Rect::cell(6, 2);              // single cell, shorthand for cols(6).rows(2)
143/// ```
144///
145/// Unbounded ends resolve against the enclosing scope, so the same rect means
146/// "to the edge" inside a subgrid too.
147#[derive(Clone, Copy, PartialEq, Eq)]
148pub struct Rect {
149    x: usize,
150    y: usize,
151    /// `None` runs to the scope's right edge.
152    w: Option<usize>,
153    /// `None` runs to the scope's bottom edge.
154    h: Option<usize>,
155}
156
157impl Rect {
158    /// Starts a rect from a column selection. Finish it with [`ColSpan::rows`].
159    ///
160    /// # Panics
161    /// If the range is empty.
162    pub fn cols(cols: impl SpanBounds) -> ColSpan {
163        let (x, w) = to_span("column", cols);
164        ColSpan { x, w }
165    }
166
167    /// Starts a rect from a row selection. Finish it with [`RowSpan::cols`].
168    ///
169    /// # Panics
170    /// If the range is empty.
171    pub fn rows(rows: impl SpanBounds) -> RowSpan {
172        let (y, h) = to_span("row", rows);
173        RowSpan { y, h }
174    }
175
176    /// A single cell at column `x`, row `y`.
177    #[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/// A [`Rect`] with only its columns given. Complete it with [`rows`](Self::rows).
205#[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    /// Completes the rect with a row selection.
214    ///
215    /// # Panics
216    /// If the range is empty.
217    #[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/// A [`Rect`] with only its rows given. Complete it with [`cols`](Self::cols).
230#[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    /// Completes the rect with a column selection.
239    ///
240    /// # Panics
241    /// If the range is empty.
242    #[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/// A [`Rect`] resolved against a concrete scope: absolute coordinates, concrete extent.
255#[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    /// Row-major index of `(x, y)` within this rect.
273    const fn local_index(self, x: usize, y: usize) -> usize {
274        (y - self.y) * self.w + (x - self.x)
275    }
276
277    /// Covered cells in row-major order.
278    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/// The sections created by a grid placement.
284#[derive(Clone, Debug)]
285pub struct Region {
286    sections: Vec<Section>,
287}
288
289impl Region {
290    /// Iterates the sections of this region.
291    pub fn iter(&self) -> Copied<slice::Iter<'_, Section>> {
292        self.sections.iter().copied()
293    }
294
295    /// Whether any section of this region contains the slot index.
296    #[must_use]
297    pub fn contains(&self, slot_index: usize) -> bool {
298        self.sections.iter().any(|s| s.contains(slot_index))
299    }
300
301    /// The region's only section.
302    ///
303    /// # Panics
304    /// If the region is not one contiguous slot range.
305    #[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
333/// What one grid cell resolved to.
334enum Cell {
335    /// A leftover `Empty` when a scope closes is a coverage error.
336    Empty,
337    /// Decoration, backed by the synthesized filler container.
338    Painted(ItemStack),
339    /// Claimed by the placement at this index into [`GridState::placements`].
340    Functional(usize),
341}
342
343/// One container-backed placement, in absolute grid coordinates.
344struct Placement {
345    rect: Abs,
346    kind: PlacementKind,
347}
348
349enum PlacementKind {
350    /// Cell `(x, y)` lowers container slot `mapping.resolve(rect.local_index(x, y))`
351    /// through `kind`.
352    Section {
353        container: ContainerRef,
354        mapping: SlotMapping,
355        kind: SectionKind,
356    },
357    /// A single fake result slot driven by a handler.
358    Result {
359        slot: Option<ResultSlot>,
360        container: ContainerRef,
361    },
362    /// Cell `(x, y)` takes `slots[rect.local_index(x, y)]`, each `Some` until flushed.
363    Slots { slots: Vec<Option<Box<dyn Slot>>> },
364}
365
366/// Maps a placement's row-major cell index to a container slot index.
367enum SlotMapping {
368    /// Cell `i` maps to container slot `offset + i`.
369    Offset(usize),
370    /// Cell `i` maps to container slot `indices[i]`.
371    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
389/// Grid-wide state shared by all nested [`GridPlacer`] scopes.
390struct GridState {
391    instance: MenuInstanceId,
392    /// Flat slot index of the grid's top-left cell in the menu.
393    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
405/// One grid scope: the whole grid, or a sub-area inside a subgrid or carve.
406struct Frame {
407    /// This scope's area, in absolute grid coordinates.
408    rect: Abs,
409    /// The axis locked in by the first `rows`/`cols` call in this scope.
410    axis: Option<Axis>,
411    /// Rows or columns already carved off.
412    cursor: usize,
413    /// Closed subgrids of this scope, in absolute grid coordinates.
414    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
428/// Places rectangles on a grid scope. Created by [`MenuBuilder::grid`].
429///
430/// All coordinates are local to this scope, which makes grids combineable.
431pub 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/// A pending placement, applied only when [`region`](Self::region) or
446/// [`result`](Self::result) is called.
447#[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    /// Maps the rect's first cell to container slot `slot` instead of 0.
458    pub fn start_at(mut self, slot: usize) -> Self {
459        self.mapping = SlotMapping::Offset(slot);
460        self
461    }
462
463    /// Maps the rect's cells (row-major) to these container slots, in the
464    /// given order. Replaces [`start_at`](Self::start_at).
465    ///
466    /// The committing call panics if the count differs from the rect's cell
467    /// count.
468    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    /// Lowers the cells through `kind`, e.g. a [`SectionKind::Custom`] factory.
474    /// The generalization of [`restrict`](Self::restrict), [`guard`](Self::guard)
475    /// and [`display`](Self::display).
476    pub fn kind(mut self, kind: impl Into<SectionKind>) -> Self {
477        self.kind = kind.into();
478        self
479    }
480
481    /// Only accepts items passing `may_place`. Pickup stays allowed.
482    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    /// Guards both placement and pickup.
490    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    /// Locks the cells as display slots. Clicks are rejected and handled in `MenuKind::on_slot_clicked`.
502    pub fn display(self) -> Self {
503        self.kind(SectionKind::Display)
504    }
505
506    /// Commits the placement and returns its region.
507    #[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    /// Commits the placement and returns its single contiguous [`Section`].
520    ///
521    /// Shorthand for [`region()`](Self::region) followed by
522    /// [`Region::single`]; use `region()` when the rect may lower to multiple
523    /// slot ranges.
524    ///
525    /// # Panics
526    /// Panics if the placement lowers to more than one contiguous slot range.
527    /// Single-cell and single-row rects can never trip this.
528    #[must_use = "hold the section to route or gate its slots"]
529    pub fn section(self) -> Section {
530        self.region().single()
531    }
532
533    /// Commits a single fake result slot driven by `handler`, ignoring `start_at` and guards.
534    ///
535    /// # Panics
536    /// If the rect is not a single cell, or the placement container differs
537    /// from [`ResultHandler::result_container`].
538    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    /// The number of columns in this scope.
552    #[must_use]
553    pub const fn width(&self) -> usize {
554        self.frame.rect.w
555    }
556
557    /// The number of rows in this scope.
558    #[must_use]
559    pub const fn height(&self) -> usize {
560        self.frame.rect.h
561    }
562
563    /// The rect covering this whole scope.
564    #[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    /// The resolved size of `rect` as `(columns, rows)`, unbounded ends run to the edges.
575    ///
576    /// # Panics
577    /// If the rect does not fit this scope.
578    #[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    /// Starts placing slots for `container` over `rect`, first cell at container slot 0.
585    ///
586    /// Configure the covered container slots with [`start_at`](PlacementBuilder::start_at) or
587    /// [`at_indices`](PlacementBuilder::at_indices), the slot behavior with
588    /// [`restrict`](PlacementBuilder::restrict), [`guard`](PlacementBuilder::guard),
589    /// [`display`](PlacementBuilder::display) or [`kind`](PlacementBuilder::kind), then commit with
590    /// [`section`](PlacementBuilder::section), [`region`](PlacementBuilder::region) or
591    /// [`result`](PlacementBuilder::result).
592    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    /// # Panics
607    /// If the rect exceeds this scope, overlaps another placement or subgrid,
608    /// the container is too small, or an explicit index mapping does not match
609    /// the rect's cell count.
610    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    /// Adds concrete pre-built slots over `rect`, consumed in row-major order.
629    ///
630    /// The menu derives its lock set from each slot's
631    /// [`SlotStorage`](crate::inventory::slots::SlotStorage).
632    /// Use [`place_boxed_slots`](Self::place_boxed_slots) for a heterogeneous or
633    /// already-erased collection.
634    ///
635    /// # Panics
636    /// If the slot count differs from the rect's cell count, or on the overlap/bounds conditions of [`place`](Self::place).
637    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    /// Adds heterogeneous or already-erased slots over `rect` in row-major order.
650    ///
651    /// # Panics
652    /// If the slot count differs from the rect's cell count, or on the overlap/bounds conditions of [`place`](Self::place).
653    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    /// # Panics
672    /// If the rect is not a single cell, or on the overlap/bounds conditions of a placement.
673    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    /// Paints decoration over `rect`. Painted cells become locked display slots of one filler container.
692    ///
693    /// Paint is the bottom layer. Placements and subgrids mask it regardless of call order, and the last paint on a cell wins.
694    ///
695    /// # Panics
696    /// If the rect exceeds this scope.
697    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    /// Paints the whole scope.
712    pub fn paint_all(&mut self, stack: impl Into<ItemStack>) {
713        self.paint(self.full(), stack);
714    }
715
716    /// Runs `f` against the sub-area `rect` with its own local coordinates.
717    ///
718    /// Self-contained. It must fully cover its own area, parent paint does not reach in, and nothing may be placed over it afterwards.
719    ///
720    /// # Panics
721    /// If the rect exceeds this scope or overlaps a placement or subgrid, or if `f` leaves cells uncovered.
722    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                // The subgrid owns its area and must cover it itself.
739                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    /// Carves the next `count` rows off this scope and runs `f` against them.
755    ///
756    /// # Panics
757    /// If `carve_cols` was already used here, if fewer than `count` rows remain, or on the [`subgrid`](Self::subgrid) conditions.
758    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    /// Carves the next `count` columns off this scope and runs `f` against them.
763    ///
764    /// # Panics
765    /// If `carve_rows` was already used here, if fewer than `count` columns remain, or on the [`subgrid`](Self::subgrid) conditions.
766    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    /// Carves everything remaining on the current axis and runs `f` against it.
771    ///
772    /// # Panics
773    /// If nothing remains to carve, or on the [`subgrid`](Self::subgrid) conditions.
774    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    /// Resolves a scope-local rect to absolute coordinates, unbounded ends running to the scope's edges.
832    ///
833    /// # Panics
834    /// If the rect does not fit this scope.
835    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    /// Whether the cell `(x, y)` lies in a closed subgrid of this scope.
854    fn in_sealed(&self, x: usize, y: usize) -> bool {
855        self.frame.sealed.iter().any(|r| r.contains_cell(x, y))
856    }
857
858    /// Claims `rect` for a placement and mints its region.
859    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    /// Mints the sections covering `abs`, one per row, with flat-adjacent rows merged.
889    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    /// Panics if any cell of this scope is still [`Cell::Empty`].
907    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    /// Asserts that the mapping fits the placement's rect and the container.
922    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    /// Runs `f` against a fresh 9-wide, `rows`-tall grid and appends its slots in row-major order.
958    ///
959    /// Grids compose. Each call covers the next `rows` rows of the menu. See the
960    /// [module documentation](self) for placement rules and an example.
961    ///
962    /// # Panics
963    /// If `rows` is zero, if the slots so far do not fill complete rows, or if `f` leaves cells neither placed nor painted.
964    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    /// Emits the resolved grid cells as menu slots in row-major order.
995    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(&region), 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(&region), 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(&region), 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(&region), 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(&region), 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}