use crate::geometry::Size;
use crate::layout::{Axis, Cell, Extent, Inset, Placement, Track};
use super::build::{
build_composition_grid, build_single_patch, element_contains_patch_id, inset_is_zero,
BuildState,
};
use super::{CompositionLayout, Patch, PatchPlacement, Slot, Span, PANEL_COL, PANEL_ROW};
pub struct Composition {
pub(super) placements: Vec<CompositionPlacement>,
pub(super) cols: usize,
pub(super) rows: usize,
pub(super) widths: Vec<Track>,
pub(super) heights: Vec<Track>,
pub(super) id: Option<String>,
pub(super) chrome: Vec<PatchPlacement>,
pub(super) aspect: Option<(f64, f64)>,
pub(super) margin: Inset,
pub(super) padding: Inset,
pub(super) error: Option<CompositionError>,
}
pub(crate) struct CompositionPlacement {
pub(super) row: u16,
pub(super) col: u16,
pub(super) span: Span,
pub(super) element: Element,
}
#[allow(clippy::large_enum_variant)]
pub enum Element {
Patch(Patch),
Composition(Composition),
}
impl From<Patch> for Element {
fn from(p: Patch) -> Self {
Element::Patch(p)
}
}
impl From<Composition> for Element {
fn from(c: Composition) -> Self {
Element::Composition(c)
}
}
impl Composition {
pub fn empty(rows: usize, cols: usize) -> Composition {
let error = (rows < 1 || cols < 1).then_some(CompositionError::Degenerate { rows, cols });
let (rows, cols) = (rows.max(1), cols.max(1));
Composition {
placements: Vec::new(),
cols,
rows,
widths: vec![Track::Fr(1.0); cols],
heights: vec![Track::Fr(1.0); rows],
id: None,
chrome: Vec::new(),
aspect: None,
margin: Inset::default(),
padding: Inset::default(),
error,
}
}
fn fail(mut self, err: CompositionError) -> Self {
if self.error.is_none() {
self.error = Some(err);
}
self
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn slot(mut self, s: Slot, cell: Cell) -> Self {
if matches!(s, Slot::Panel) {
return self.fail(CompositionError::PanelSlot);
}
let (r, c, rs, cs) = s.placement();
self.chrome.push(PatchPlacement {
placement: Placement::at(r, c).span(rs, cs),
region: s.name().to_string(),
cell,
});
self
}
pub fn place_at(
mut self,
region: impl Into<String>,
row: u16,
col: u16,
span: Span,
cell: Cell,
) -> Self {
let end_row = row + span.rows - 1;
let end_col = col + span.cols - 1;
if row <= PANEL_ROW && end_row >= PANEL_ROW && col <= PANEL_COL && end_col >= PANEL_COL {
return self.fail(CompositionError::PanelCovered {
row: PANEL_ROW,
col: PANEL_COL,
});
}
self.chrome.push(PatchPlacement {
placement: Placement::at(row, col).span(span.rows, span.cols),
region: region.into(),
cell,
});
self
}
pub fn aspect(mut self, w: f64, h: f64) -> Self {
self.aspect = Some((w, h));
self
}
pub fn margin(mut self, inset: Inset) -> Self {
self.margin = inset;
self
}
pub fn margin_all(self, length: Extent) -> Self {
self.margin(Inset::all(length))
}
pub fn padding(mut self, inset: Inset) -> Self {
self.padding = inset;
self
}
pub fn padding_all(self, length: Extent) -> Self {
self.padding(Inset::all(length))
}
pub fn composition_id(&self) -> Option<&str> {
self.id.as_deref()
}
pub fn aspect_ratio(&self) -> Option<(f64, f64)> {
self.aspect
}
pub fn margin_inset(&self) -> &Inset {
&self.margin
}
pub fn padding_inset(&self) -> &Inset {
&self.padding
}
pub(super) fn has_chrome(&self) -> bool {
!self.chrome.is_empty() || !inset_is_zero(&self.margin) || !inset_is_zero(&self.padding)
}
pub fn place(mut self, row: u16, col: u16, span: Span, element: impl Into<Element>) -> Self {
if row < 1 || col < 1 {
return self.fail(CompositionError::NotOneIndexed { row, col });
}
let end_row = (row + span.rows - 1) as usize;
let end_col = (col + span.cols - 1) as usize;
let (rows, cols) = (self.rows, self.cols);
if end_row > rows {
return self.fail(CompositionError::PlacementOverflow {
axis: Axis::Height,
end: end_row,
available: rows,
});
}
if end_col > cols {
return self.fail(CompositionError::PlacementOverflow {
axis: Axis::Width,
end: end_col,
available: cols,
});
}
self.placements.push(CompositionPlacement {
row,
col,
span,
element: element.into(),
});
self
}
pub fn widths(mut self, tracks: Vec<Track>) -> Self {
if tracks.len() != self.cols {
let found = tracks.len();
let expected = self.cols;
return self.fail(CompositionError::TrackCountMismatch {
axis: Axis::Width,
expected,
found,
});
}
self.widths = tracks;
self
}
pub fn heights(mut self, tracks: Vec<Track>) -> Self {
if tracks.len() != self.rows {
let found = tracks.len();
let expected = self.rows;
return self.fail(CompositionError::TrackCountMismatch {
axis: Axis::Height,
expected,
found,
});
}
self.heights = tracks;
self
}
pub fn contains_patch_id(&self, id: &str) -> bool {
self.placements
.iter()
.any(|p| element_contains_patch_id(&p.element, id))
}
pub fn cols(&self) -> usize {
self.cols
}
pub fn rows(&self) -> usize {
self.rows
}
pub fn widths_slice(&self) -> &[Track] {
&self.widths
}
pub fn heights_slice(&self) -> &[Track] {
&self.heights
}
pub fn placements(&self) -> impl Iterator<Item = (u16, u16, Span, &Element)> + '_ {
self.placements
.iter()
.map(|p| (p.row, p.col, p.span, &p.element))
}
pub fn append_col(mut self, other: impl Into<Element>) -> Self {
if self.rows != 1 {
let extent = self.rows;
return self.fail(CompositionError::NotAppendable {
axis: Axis::Height,
extent,
});
}
self.cols += 1;
self.widths.push(Track::Fr(1.0));
self.placements.push(CompositionPlacement {
row: 1,
col: self.cols as u16,
span: Span::cell(),
element: other.into(),
});
self
}
pub fn append_row(mut self, other: impl Into<Element>) -> Self {
if self.cols != 1 {
let extent = self.cols;
return self.fail(CompositionError::NotAppendable {
axis: Axis::Width,
extent,
});
}
self.rows += 1;
self.heights.push(Track::Fr(1.0));
self.placements.push(CompositionPlacement {
row: self.rows as u16,
col: 1,
span: Span::cell(),
element: other.into(),
});
self
}
pub fn solve(self, size: Size, dpi: f64) -> CompositionLayout {
Element::Composition(self).solve(size, dpi)
}
pub fn try_solve(self, size: Size, dpi: f64) -> Result<CompositionLayout, CompositionError> {
Element::Composition(self).try_solve(size, dpi)
}
pub fn error(&self) -> Option<&CompositionError> {
self.error.as_ref()
}
}
impl Element {
fn check_construction(&self) -> Result<(), CompositionError> {
match self {
Element::Patch(_) => Ok(()),
Element::Composition(c) => {
if let Some(e) = &c.error {
return Err(e.clone());
}
for p in &c.placements {
p.element.check_construction()?;
}
Ok(())
}
}
}
pub fn solve(self, size: Size, dpi: f64) -> CompositionLayout {
match self.try_solve(size, dpi) {
Ok(layout) => layout,
Err(e) => panic!("composition error: {e} — use try_solve to handle this"),
}
}
pub fn try_solve(self, size: Size, dpi: f64) -> Result<CompositionLayout, CompositionError> {
self.check_construction()?;
let mut state = BuildState::new();
let root_id = state.alloc_id();
let grid = match self {
Element::Patch(p) => build_single_patch(p, root_id, &mut state)?,
Element::Composition(c) => build_composition_grid(c, root_id, &mut state, None)?,
};
let layout = grid.solve(size, dpi);
Ok(CompositionLayout {
layout,
regions: state.regions,
})
}
}
#[derive(Debug, Clone)]
pub enum CompositionError {
DuplicateId(String),
Degenerate { rows: usize, cols: usize },
PanelSlot,
PanelCovered { row: u16, col: u16 },
NotOneIndexed { row: u16, col: u16 },
PlacementOverflow {
axis: Axis,
end: usize,
available: usize,
},
TrackCountMismatch {
axis: Axis,
expected: usize,
found: usize,
},
NotAppendable { axis: Axis, extent: usize },
CellCountMismatch {
rows: usize,
cols: usize,
found: usize,
},
}
impl std::fmt::Display for CompositionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompositionError::DuplicateId(id) => {
write!(f, "duplicate patch id: {id:?}")
}
CompositionError::Degenerate { rows, cols } => {
write!(f, "composition must be at least 1×1, got {rows}×{cols}")
}
CompositionError::PanelSlot => write!(
f,
"Composition::slot does not accept Slot::Panel; the composition's facets fill it"
),
CompositionError::PanelCovered { row, col } => write!(
f,
"Composition::place_at cannot cover the panel cell (row {row}, col {col}); \
the facets fill it"
),
CompositionError::NotOneIndexed { row, col } => {
write!(f, "composition placement is 1-indexed, got ({row}, {col})")
}
CompositionError::PlacementOverflow {
axis,
end,
available,
} => write!(
f,
"placement reaches {axis:?} {end} but the composition has {available}"
),
CompositionError::TrackCountMismatch {
axis,
expected,
found,
} => write!(
f,
"{axis:?} track list must have {expected} entries, got {found}"
),
CompositionError::NotAppendable { axis, extent } => write!(
f,
"appending along {axis:?} requires a single-track composition, got {extent}"
),
CompositionError::CellCountMismatch { rows, cols, found } => write!(
f,
"grid({rows}, {cols}) needs {} cells, got {found}",
rows * cols
),
}
}
}
impl std::error::Error for CompositionError {}
pub fn beside(a: impl Into<Element>, b: impl Into<Element>) -> Composition {
grid(1, 2, vec![a.into(), b.into()])
}
pub fn stack(a: impl Into<Element>, b: impl Into<Element>) -> Composition {
grid(2, 1, vec![a.into(), b.into()])
}
pub fn grid(rows: usize, cols: usize, cells: Vec<Element>) -> Composition {
let found = cells.len();
let mut c = Composition::empty(rows, cols);
if found != rows * cols {
return c.fail(CompositionError::CellCountMismatch { rows, cols, found });
}
for (i, element) in cells.into_iter().enumerate() {
let r = (i / cols) as u16 + 1;
let col = (i % cols) as u16 + 1;
c.placements.push(CompositionPlacement {
row: r,
col,
span: Span::cell(),
element,
});
}
c
}
pub fn spacer() -> Patch {
Patch::anonymous()
}
pub fn wrap(id: impl Into<String>, cell: Cell) -> Patch {
Patch::new(id).slot(Slot::Panel, cell)
}