pub const HEADER_HEIGHT: f32 = 20.0;
pub const ROWS_LAYOUT_TOP: f32 = 24.0;
pub const ROWS_SECTION_LABEL_HEIGHT: f32 = 14.0;
pub const ROWS_COLUMN_GAP: f32 = 7.0;
pub const ROWS_ROW_GAP: f32 = 19.0;
pub const DROPDOWN_BOX_HEIGHT: f32 = 20.0;
use truce_core::cast::len_u32;
#[derive(Clone, Debug)]
pub struct KnobDef {
pub param_id: u32,
pub label: &'static str,
pub widget: Option<WidgetKind>,
pub span: u32,
pub param_id_y: Option<u32>,
pub meter_ids: Option<Vec<u32>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WidgetKind {
Knob,
Slider,
Toggle,
Selector,
Dropdown,
Meter,
XYPad,
}
impl KnobDef {
pub fn knob(param_id: impl Into<u32>, label: &'static str) -> Self {
Self {
param_id: param_id.into(),
label,
widget: Some(WidgetKind::Knob),
span: 1,
param_id_y: None,
meter_ids: None,
}
}
pub fn slider(param_id: impl Into<u32>, label: &'static str) -> Self {
Self {
param_id: param_id.into(),
label,
widget: Some(WidgetKind::Slider),
span: 1,
param_id_y: None,
meter_ids: None,
}
}
pub fn toggle(param_id: impl Into<u32>, label: &'static str) -> Self {
Self {
param_id: param_id.into(),
label,
widget: Some(WidgetKind::Toggle),
span: 1,
param_id_y: None,
meter_ids: None,
}
}
pub fn selector(param_id: impl Into<u32>, label: &'static str) -> Self {
Self {
param_id: param_id.into(),
label,
widget: Some(WidgetKind::Selector),
span: 1,
param_id_y: None,
meter_ids: None,
}
}
pub fn dropdown(param_id: impl Into<u32>, label: &'static str) -> Self {
Self {
param_id: param_id.into(),
label,
widget: Some(WidgetKind::Dropdown),
span: 1,
param_id_y: None,
meter_ids: None,
}
}
#[must_use]
pub fn meter(ids: &[u32], label: &'static str) -> Self {
Self {
param_id: ids.first().copied().unwrap_or(0),
label,
widget: Some(WidgetKind::Meter),
span: 1,
param_id_y: None,
meter_ids: Some(ids.to_vec()),
}
}
pub fn xy_pad(param_x: impl Into<u32>, param_y: impl Into<u32>, label: &'static str) -> Self {
Self {
param_id: param_x.into(),
label,
widget: Some(WidgetKind::XYPad),
span: 2,
param_id_y: Some(param_y.into()),
meter_ids: None,
}
}
#[must_use]
pub fn with_span(mut self, span: u32) -> Self {
self.span = span;
self
}
}
#[derive(Clone, Debug)]
pub struct KnobRow {
pub label: Option<&'static str>,
pub knobs: Vec<KnobDef>,
}
#[derive(Clone, Debug)]
pub struct PluginLayout {
pub titles: HeaderTitles,
pub rows: Vec<KnobRow>,
pub width: u32,
pub height: u32,
pub knob_size: f32,
}
impl PluginLayout {
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
#[must_use]
pub fn compute_size(rows: &[KnobRow], knob_size: f32, titles: &HeaderTitles) -> (u32, u32) {
let header_h = if titles.is_empty() { 0.0 } else { 21.0 };
let row_h = knob_size + 19.0;
let section_label_h = 14.0;
let padding = 7.0;
let max_knobs = rows
.iter()
.map(|r| {
r.knobs
.iter()
.map(|k| k.span.max(1) as usize)
.sum::<usize>()
})
.max()
.unwrap_or(1);
let w = max_knobs as f32 * (knob_size + 7.0) + 13.0;
let mut h = header_h + padding;
for row in rows {
if row.label.is_some() {
h += section_label_h;
}
h += row_h + padding;
}
(w as u32, h as u32)
}
#[must_use]
pub fn build(titles: HeaderTitles, rows: Vec<KnobRow>, knob_size: f32) -> Self {
let (w, h) = Self::compute_size(&rows, knob_size, &titles);
Self {
titles,
rows,
width: w,
height: h,
knob_size,
}
}
}
pub const AUTO: u32 = u32::MAX;
pub const GRID_GAP: f32 = 19.0;
pub const GRID_PADDING: f32 = 10.0;
pub const GRID_HEADER_H: f32 = 21.0;
pub const GRID_SECTION_H: f32 = 14.0;
#[derive(Clone, Debug)]
pub struct GridWidget {
pub col: u32,
pub row: u32,
pub col_span: u32,
pub row_span: u32,
pub param_id: u32,
pub label: &'static str,
pub widget: Option<WidgetKind>,
pub param_id_y: Option<u32>,
pub meter_ids: Option<Vec<u32>>,
}
impl GridWidget {
pub fn knob(param_id: impl Into<u32>, label: &'static str) -> Self {
Self {
col: AUTO,
row: AUTO,
col_span: 1,
row_span: 1,
param_id: param_id.into(),
label,
widget: Some(WidgetKind::Knob),
param_id_y: None,
meter_ids: None,
}
}
pub fn slider(param_id: impl Into<u32>, label: &'static str) -> Self {
Self {
col: AUTO,
row: AUTO,
col_span: 1,
row_span: 1,
param_id: param_id.into(),
label,
widget: Some(WidgetKind::Slider),
param_id_y: None,
meter_ids: None,
}
}
pub fn toggle(param_id: impl Into<u32>, label: &'static str) -> Self {
Self {
col: AUTO,
row: AUTO,
col_span: 1,
row_span: 1,
param_id: param_id.into(),
label,
widget: Some(WidgetKind::Toggle),
param_id_y: None,
meter_ids: None,
}
}
pub fn selector(param_id: impl Into<u32>, label: &'static str) -> Self {
Self {
col: AUTO,
row: AUTO,
col_span: 1,
row_span: 1,
param_id: param_id.into(),
label,
widget: Some(WidgetKind::Selector),
param_id_y: None,
meter_ids: None,
}
}
pub fn dropdown(param_id: impl Into<u32>, label: &'static str) -> Self {
Self {
col: AUTO,
row: AUTO,
col_span: 1,
row_span: 1,
param_id: param_id.into(),
label,
widget: Some(WidgetKind::Dropdown),
param_id_y: None,
meter_ids: None,
}
}
#[must_use]
pub fn meter(ids: &[u32], label: &'static str) -> Self {
Self {
col: AUTO,
row: AUTO,
col_span: 1,
row_span: 1,
param_id: ids.first().copied().unwrap_or(0),
label,
widget: Some(WidgetKind::Meter),
param_id_y: None,
meter_ids: Some(ids.to_vec()),
}
}
pub fn xy_pad(param_x: impl Into<u32>, param_y: impl Into<u32>, label: &'static str) -> Self {
Self {
col: AUTO,
row: AUTO,
col_span: 2,
row_span: 2,
param_id: param_x.into(),
label,
widget: Some(WidgetKind::XYPad),
param_id_y: Some(param_y.into()),
meter_ids: None,
}
}
#[must_use]
pub fn cols(mut self, n: u32) -> Self {
self.col_span = n;
self
}
#[must_use]
pub fn rows(mut self, n: u32) -> Self {
self.row_span = n;
self
}
#[must_use]
pub fn at(mut self, col: u32, row: u32) -> Self {
self.col = col;
self.row = row;
self
}
}
#[derive(Clone, Debug)]
pub struct Section {
pub label: Option<&'static str>,
pub widgets: Vec<GridWidget>,
}
#[must_use]
pub fn section(label: &'static str, widgets: Vec<GridWidget>) -> Section {
Section {
label: Some(label),
widgets,
}
}
#[must_use]
pub fn widgets(widgets: Vec<GridWidget>) -> Section {
Section {
label: None,
widgets,
}
}
pub fn knob(param_id: impl Into<u32>, label: &'static str) -> GridWidget {
GridWidget::knob(param_id, label)
}
pub fn slider(param_id: impl Into<u32>, label: &'static str) -> GridWidget {
GridWidget::slider(param_id, label)
}
pub fn toggle(param_id: impl Into<u32>, label: &'static str) -> GridWidget {
GridWidget::toggle(param_id, label)
}
pub fn selector(param_id: impl Into<u32>, label: &'static str) -> GridWidget {
GridWidget::selector(param_id, label)
}
pub fn dropdown(param_id: impl Into<u32>, label: &'static str) -> GridWidget {
GridWidget::dropdown(param_id, label)
}
pub fn meter<I: Into<u32> + Copy>(ids: &[I], label: &'static str) -> GridWidget {
let u32_ids: Vec<u32> = ids.iter().map(|id| (*id).into()).collect();
GridWidget::meter(&u32_ids, label)
}
pub fn xy_pad(param_x: impl Into<u32>, param_y: impl Into<u32>, label: &'static str) -> GridWidget {
GridWidget::xy_pad(param_x, param_y, label)
}
impl From<GridWidget> for Section {
fn from(w: GridWidget) -> Self {
Section {
label: None,
widgets: vec![w],
}
}
}
#[derive(Clone, Debug, Default)]
pub struct HeaderTitles {
pub title: Option<&'static str>,
pub subtitle: Option<&'static str>,
}
impl HeaderTitles {
#[must_use]
pub const fn none() -> Self {
Self {
title: None,
subtitle: None,
}
}
#[must_use]
pub const fn title(s: &'static str) -> Self {
Self {
title: Some(s),
subtitle: None,
}
}
#[must_use]
pub const fn subtitle(s: &'static str) -> Self {
Self {
title: None,
subtitle: Some(s),
}
}
#[must_use]
pub const fn pair(title: &'static str, subtitle: &'static str) -> Self {
Self {
title: Some(title),
subtitle: Some(subtitle),
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.title.is_none() && self.subtitle.is_none()
}
}
#[derive(Clone, Debug)]
pub struct GridLayout {
pub titles: HeaderTitles,
pub cols: u32,
pub sections: Vec<(u32, &'static str)>,
pub widgets: Vec<GridWidget>,
pub cell_size: f32,
pub width: u32,
pub height: u32,
original_widgets: Vec<GridWidget>,
original_breaks: Vec<(usize, &'static str)>,
}
pub const GRID_DEFAULT_CELL_SIZE: f32 = 50.0;
impl GridLayout {
#[must_use]
pub fn build(entries: Vec<Section>) -> Self {
let mut widgets = Vec::new();
let mut breaks = Vec::new();
let mut max_widgets_per_section = 0usize;
for s in entries {
if let Some(label) = s.label {
breaks.push((widgets.len(), label));
}
max_widgets_per_section = max_widgets_per_section.max(s.widgets.len());
widgets.extend(s.widgets);
}
let max_explicit_col = widgets
.iter()
.filter(|w| w.col != AUTO)
.map(|w| w.col + w.col_span)
.max()
.unwrap_or(0);
let cols = len_u32(max_widgets_per_section)
.max(max_explicit_col)
.max(1);
let mut layout = Self {
titles: HeaderTitles::none(),
cols,
sections: Vec::new(),
widgets: widgets.clone(),
cell_size: GRID_DEFAULT_CELL_SIZE,
width: 0,
height: 0,
original_widgets: widgets,
original_breaks: breaks,
};
layout.flow_and_size();
layout
}
#[must_use]
pub fn with_cols(mut self, cols: u32) -> Self {
self.cols = cols.max(1);
self.flow_and_size();
self
}
#[must_use]
pub fn with_cell_size(mut self, cell_size: f32) -> Self {
self.cell_size = cell_size;
let (w, h) = self.compute_size();
self.width = w;
self.height = h;
self
}
#[must_use]
pub fn with_grid(mut self, cols: u32, cell_size: f32) -> Self {
self = self.with_cell_size(cell_size);
self.with_cols(cols)
}
#[must_use]
pub fn with_titles(mut self, titles: HeaderTitles) -> Self {
self.titles = titles;
let (w, h) = self.compute_size();
self.width = w;
self.height = h;
self
}
#[must_use]
pub fn with_title(mut self, title: &'static str) -> Self {
self.titles.title = Some(title);
let (w, h) = self.compute_size();
self.width = w;
self.height = h;
self
}
#[must_use]
pub fn with_subtitle(mut self, subtitle: &'static str) -> Self {
self.titles.subtitle = Some(subtitle);
let (w, h) = self.compute_size();
self.width = w;
self.height = h;
self
}
pub(crate) fn header_height(&self) -> f32 {
if self.titles.is_empty() {
0.0
} else {
GRID_HEADER_H
}
}
fn flow_and_size(&mut self) {
self.widgets = self.original_widgets.clone();
self.sections.clear();
let breaks: Vec<(usize, &'static str)> = self.original_breaks.clone();
self.auto_flow_with_breaks(&breaks);
let (w, h) = self.compute_size();
self.width = w;
self.height = h;
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
#[must_use]
pub fn compute_size(&self) -> (u32, u32) {
let max_col = self
.widgets
.iter()
.map(|w| w.col + w.col_span)
.max()
.unwrap_or(1);
let max_row = self
.widgets
.iter()
.map(|w| w.row + w.row_span)
.max()
.unwrap_or(1);
let section_count = self.sections.len() as f32;
let w = GRID_PADDING * 2.0 + max_col as f32 * (self.cell_size + GRID_GAP) - GRID_GAP;
let bottom_label_h = 22.0; let h = self.header_height() + GRID_PADDING + max_row as f32 * (self.cell_size + GRID_GAP)
- GRID_GAP
+ section_count * GRID_SECTION_H
+ bottom_label_h
+ GRID_PADDING;
(w as u32, h as u32)
}
pub(crate) fn auto_flow_with_breaks(&mut self, breaks: &[(usize, &'static str)]) {
let mut occupied = std::collections::HashSet::new();
let mut cursor_col: u32 = 0;
let mut cursor_row: u32 = 0;
let mut any_emitted = false;
for w in &self.widgets {
if w.col != AUTO && w.row != AUTO {
for c in w.col..w.col + w.col_span {
for r in w.row..w.row + w.row_span {
occupied.insert((c, r));
}
}
}
}
for (i, w) in self.widgets.iter_mut().enumerate() {
for &(break_idx, label) in breaks {
if break_idx == i {
if any_emitted || cursor_col > 0 {
cursor_row += 1;
cursor_col = 0;
}
self.sections.push((cursor_row, label));
any_emitted = true;
}
}
if w.col != AUTO && w.row != AUTO {
any_emitted = true;
continue;
}
loop {
if cursor_col + w.col_span > self.cols {
cursor_col = 0;
cursor_row += 1;
}
let fits = (0..w.col_span).all(|dc| {
(0..w.row_span)
.all(|dr| !occupied.contains(&(cursor_col + dc, cursor_row + dr)))
});
if fits {
break;
}
cursor_col += 1;
}
w.col = cursor_col;
w.row = cursor_row;
for c in w.col..w.col + w.col_span {
for r in w.row..w.row + w.row_span {
occupied.insert((c, r));
}
}
cursor_col += w.col_span;
any_emitted = true;
}
}
}
#[must_use]
pub fn compute_section_offsets(layout: &GridLayout) -> Vec<f32> {
let max_row = layout
.widgets
.iter()
.map(|w| w.row + w.row_span)
.max()
.unwrap_or(1);
let mut offsets = vec![0.0f32; max_row as usize + 1];
let mut cumulative = 0.0;
for row in 0..=max_row {
if layout.sections.iter().any(|(r, _)| *r == row) {
cumulative += GRID_SECTION_H;
}
if (row as usize) < offsets.len() {
offsets[row as usize] = cumulative;
}
}
offsets
}
impl From<PluginLayout> for GridLayout {
fn from(pl: PluginLayout) -> Self {
let cols = pl
.rows
.iter()
.map(|r| r.knobs.iter().map(|k| k.span.max(1)).sum::<u32>())
.max()
.unwrap_or(1);
let mut widgets = Vec::new();
let mut sections = Vec::new();
for (grid_row, row) in pl.rows.iter().enumerate() {
let grid_row = len_u32(grid_row);
if let Some(label) = row.label {
sections.push((grid_row, label));
}
let mut col = 0u32;
for knob in &row.knobs {
widgets.push(GridWidget {
col,
row: grid_row,
col_span: knob.span.max(1),
row_span: 1,
param_id: knob.param_id,
label: knob.label,
widget: knob.widget,
param_id_y: knob.param_id_y,
meter_ids: knob.meter_ids.clone(),
});
col += knob.span.max(1);
}
}
let mut gl = GridLayout {
titles: pl.titles.clone(),
cols,
sections,
widgets: widgets.clone(),
cell_size: pl.knob_size,
width: 0,
height: 0,
original_widgets: widgets,
original_breaks: Vec::new(),
};
let (w, h) = gl.compute_size();
gl.width = w;
gl.height = h;
gl
}
}
#[derive(Clone, Debug)]
pub enum Layout {
Rows(PluginLayout),
Grid(GridLayout),
}
impl Layout {
#[must_use]
pub fn width(&self) -> u32 {
match self {
Layout::Rows(l) => l.width,
Layout::Grid(g) => g.width,
}
}
#[must_use]
pub fn height(&self) -> u32 {
match self {
Layout::Rows(l) => l.height,
Layout::Grid(g) => g.height,
}
}
#[must_use]
pub fn title(&self) -> Option<&str> {
match self {
Layout::Rows(l) => l.titles.title,
Layout::Grid(g) => g.titles.title,
}
}
#[must_use]
pub fn subtitle(&self) -> Option<&str> {
match self {
Layout::Rows(l) => l.titles.subtitle,
Layout::Grid(g) => g.titles.subtitle,
}
}
}