Fix layout

This commit is contained in:
2026-02-02 12:18:22 +01:00
parent 2af0b67714
commit 7348bd38b1
12 changed files with 294 additions and 65 deletions

View File

@@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file.
- Double-stack words: `2dup`, `2drop`, `2swap`, `2over`.
- `forget` word to remove user-defined words from the dictionary.
### Fixed
- Scope/spectrum ratio asymmetry in Left/Right layout modes.
## [0.0.3] - 2026-02-02
### Added

View File

@@ -0,0 +1,73 @@
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::Widget;
pub struct ActivePatterns<'a> {
patterns: &'a [(usize, usize, usize)], // (bank, pattern, iter)
current_step: Option<(usize, usize)>, // (current_step, total_steps)
}
impl<'a> ActivePatterns<'a> {
pub fn new(patterns: &'a [(usize, usize, usize)]) -> Self {
Self {
patterns,
current_step: None,
}
}
pub fn with_step(mut self, current: usize, total: usize) -> Self {
self.current_step = Some((current, total));
self
}
}
impl Widget for ActivePatterns<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width < 10 || area.height == 0 {
return;
}
let theme = theme::get();
let max_pattern_rows = if self.current_step.is_some() {
area.height.saturating_sub(1) as usize
} else {
area.height as usize
};
for (row, &(bank, pattern, iter)) in self.patterns.iter().enumerate() {
if row >= max_pattern_rows {
break;
}
let text = format!("B{:02}:{:02} ({:02})", bank + 1, pattern + 1, iter.min(99));
let y = area.y + row as u16;
let bg = if row % 2 == 0 {
theme.table.row_even
} else {
theme.table.row_odd
};
let mut chars = text.chars();
for col in 0..area.width as usize {
let ch = chars.next().unwrap_or(' ');
buf[(area.x + col as u16, y)]
.set_char(ch)
.set_fg(theme.ui.text_primary)
.set_bg(bg);
}
}
if let Some((current, total)) = self.current_step {
let text = format!("{:02}/{:02}", current + 1, total);
let y = area.y + area.height.saturating_sub(1);
let mut chars = text.chars();
for col in 0..area.width as usize {
let ch = chars.next().unwrap_or(' ');
buf[(area.x + col as u16, y)]
.set_char(ch)
.set_fg(theme.ui.text_primary)
.set_bg(theme.table.row_even);
}
}
}
}

View File

@@ -1,3 +1,4 @@
mod active_patterns;
mod confirm;
mod editor;
mod file_browser;
@@ -12,6 +13,7 @@ mod text_input;
pub mod theme;
mod vu_meter;
pub use active_patterns::ActivePatterns;
pub use confirm::ConfirmModal;
pub use editor::{CompletionCandidate, Editor};
pub use file_browser::FileBrowserModal;

View File

@@ -110,6 +110,7 @@ impl App {
show_completion: self.ui.show_completion,
flash_brightness: self.ui.flash_brightness,
color_scheme: self.ui.color_scheme,
layout: self.audio.config.layout,
..Default::default()
},
link: crate::settings::LinkSettings {

View File

@@ -11,8 +11,8 @@ use crate::engine::{AudioCommand, LinkState, SeqCommand, SequencerSnapshot};
use crate::model::PatternSpeed;
use crate::page::Page;
use crate::state::{
DeviceKind, EngineSection, Modal, OptionsFocus, PanelFocus, PatternField, PatternPropsField,
SampleBrowserState, SettingKind, SidePanel,
CyclicEnum, DeviceKind, EngineSection, Modal, OptionsFocus, PanelFocus, PatternField,
PatternPropsField, SampleBrowserState, SettingKind, SidePanel,
};
pub enum InputResult {
@@ -956,6 +956,9 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
name: current_name,
}));
}
KeyCode::Char('o') => {
ctx.app.audio.config.layout = ctx.app.audio.config.layout.next();
}
KeyCode::Char('?') => {
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
}

View File

@@ -100,6 +100,7 @@ fn main() -> io::Result<()> {
app.ui.show_completion = settings.display.show_completion;
app.ui.flash_brightness = settings.display.flash_brightness;
app.ui.color_scheme = settings.display.color_scheme;
app.audio.config.layout = settings.display.layout;
theme::set(settings.display.color_scheme.to_theme());
// Load MIDI settings

View File

@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::state::ColorScheme;
use crate::state::{ColorScheme, MainLayout};
const APP_NAME: &str = "cagire";
@@ -50,6 +50,8 @@ pub struct DisplaySettings {
pub font: String,
#[serde(default)]
pub color_scheme: ColorScheme,
#[serde(default)]
pub layout: MainLayout,
}
fn default_font() -> String {
@@ -91,6 +93,7 @@ impl Default for DisplaySettings {
flash_brightness: 1.0,
font: default_font(),
color_scheme: ColorScheme::default(),
layout: MainLayout::default(),
}
}
}

View File

@@ -1,8 +1,22 @@
use doux::audio::AudioDeviceInfo;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use super::CyclicEnum;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum MainLayout {
#[default]
Top,
Bottom,
Left,
Right,
}
impl CyclicEnum for MainLayout {
const VARIANTS: &'static [Self] = &[Self::Top, Self::Bottom, Self::Left, Self::Right];
}
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum RefreshRate {
#[default]
@@ -62,6 +76,7 @@ pub struct AudioConfig {
pub show_scope: bool,
pub show_spectrum: bool,
pub lookahead_ms: u32,
pub layout: MainLayout,
}
impl Default for AudioConfig {
@@ -79,6 +94,7 @@ impl Default for AudioConfig {
show_scope: true,
show_spectrum: true,
lookahead_ms: 15,
layout: MainLayout::default(),
}
}
}

View File

@@ -27,7 +27,7 @@ pub mod project;
pub mod sample_browser;
pub mod ui;
pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind};
pub use audio::{AudioSettings, DeviceKind, EngineSection, MainLayout, Metrics, SettingKind};
pub use color_scheme::ColorScheme;
pub use editor::{
CopiedStepData, CopiedSteps, EditorContext, PatternField, PatternPropsField, StackCache,

View File

@@ -1,65 +1,117 @@
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::Paragraph;
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::Frame;
use crate::app::App;
use crate::engine::SequencerSnapshot;
use crate::state::MainLayout;
use crate::theme;
use crate::widgets::{Orientation, Scope, Spectrum, VuMeter};
use crate::widgets::{ActivePatterns, Orientation, Scope, Spectrum, VuMeter};
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
let [left_area, _spacer, vu_area] = Layout::horizontal([
let [patterns_area, _, main_area, _, vu_area] = Layout::horizontal([
Constraint::Length(13),
Constraint::Length(2),
Constraint::Fill(1),
Constraint::Length(2),
Constraint::Length(8),
Constraint::Length(10),
])
.areas(area);
let show_scope = app.audio.config.show_scope;
let show_spectrum = app.audio.config.show_spectrum;
let viz_height = if show_scope || show_spectrum { 14 } else { 0 };
let has_viz = show_scope || show_spectrum;
let layout = app.audio.config.layout;
let [viz_area, sequencer_area] = Layout::vertical([
Constraint::Length(viz_height),
Constraint::Fill(1),
])
.areas(left_area);
let (viz_area, sequencer_area) = match layout {
MainLayout::Top => {
let viz_height = if has_viz { 16 } else { 0 };
let [viz, seq] = Layout::vertical([
Constraint::Length(viz_height),
Constraint::Fill(1),
])
.areas(main_area);
(viz, seq)
}
MainLayout::Bottom => {
let viz_height = if has_viz { 16 } else { 0 };
let [seq, viz] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(viz_height),
])
.areas(main_area);
(viz, seq)
}
MainLayout::Left => {
let viz_width = if has_viz { 33 } else { 0 };
let [viz, _spacer, seq] = Layout::horizontal([
Constraint::Percentage(viz_width),
Constraint::Length(2),
Constraint::Fill(1),
])
.areas(main_area);
(viz, seq)
}
MainLayout::Right => {
let viz_width = if has_viz { 33 } else { 0 };
let [seq, _spacer, viz] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(2),
Constraint::Percentage(viz_width),
])
.areas(main_area);
(viz, seq)
}
};
if show_scope && show_spectrum {
let [scope_area, _, spectrum_area] = Layout::horizontal([
Constraint::Percentage(50),
Constraint::Length(2),
Constraint::Percentage(50),
])
.areas(viz_area);
render_scope(frame, app, scope_area);
render_spectrum(frame, app, spectrum_area);
} else if show_scope {
render_scope(frame, app, viz_area);
} else if show_spectrum {
render_spectrum(frame, app, viz_area);
if has_viz {
render_viz_area(frame, app, viz_area, layout, show_scope, show_spectrum);
}
render_sequencer(frame, app, snapshot, sequencer_area);
render_vu_meter(frame, app, vu_area);
render_active_patterns(frame, app, snapshot, patterns_area);
}
// Calculate actual grid height to align VU meter
let pattern = app.current_edit_pattern();
let page = app.editor_ctx.step / STEPS_PER_PAGE;
let page_start = page * STEPS_PER_PAGE;
let steps_on_page = (page_start + STEPS_PER_PAGE).min(pattern.length) - page_start;
let num_rows = steps_on_page.div_ceil(8);
let spacing = num_rows.saturating_sub(1) as u16;
let row_height = sequencer_area.height.saturating_sub(spacing) / num_rows as u16;
let actual_grid_height = row_height * num_rows as u16 + spacing;
fn render_viz_area(
frame: &mut Frame,
app: &App,
area: Rect,
layout: MainLayout,
show_scope: bool,
show_spectrum: bool,
) {
let is_vertical_layout = matches!(layout, MainLayout::Left | MainLayout::Right);
let aligned_vu_area = Rect {
y: sequencer_area.y,
height: actual_grid_height,
..vu_area
};
render_vu_meter(frame, app, aligned_vu_area);
if show_scope && show_spectrum {
if is_vertical_layout {
let [scope_area, spectrum_area] = Layout::vertical([
Constraint::Fill(1),
Constraint::Fill(1),
])
.areas(area);
render_scope(frame, app, scope_area, Orientation::Vertical);
render_spectrum(frame, app, spectrum_area);
} else {
let [scope_area, spectrum_area] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Fill(1),
])
.areas(area);
render_scope(frame, app, scope_area, Orientation::Horizontal);
render_spectrum(frame, app, spectrum_area);
}
} else if show_scope {
let orientation = if is_vertical_layout {
Orientation::Vertical
} else {
Orientation::Horizontal
};
render_scope(frame, app, area, orientation);
} else if show_spectrum {
render_spectrum(frame, app, area);
}
}
const STEPS_PER_PAGE: usize = 32;
@@ -233,21 +285,65 @@ fn render_tile(
}
}
fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
fn render_scope(frame: &mut Frame, app: &App, area: Rect, orientation: Orientation) {
let theme = theme::get();
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.ui.border));
let inner = block.inner(area);
frame.render_widget(block, area);
let scope = Scope::new(&app.metrics.scope)
.orientation(Orientation::Horizontal)
.orientation(orientation)
.color(theme.meter.low);
frame.render_widget(scope, area);
frame.render_widget(scope, inner);
}
fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
let area = Rect { height: area.height.saturating_sub(1), ..area };
let theme = theme::get();
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.ui.border));
let inner = block.inner(area);
frame.render_widget(block, area);
let spectrum = Spectrum::new(&app.metrics.spectrum);
frame.render_widget(spectrum, area);
frame.render_widget(spectrum, inner);
}
fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.ui.border));
let inner = block.inner(area);
frame.render_widget(block, area);
let vu = VuMeter::new(app.metrics.peak_left, app.metrics.peak_right);
frame.render_widget(vu, area);
frame.render_widget(vu, inner);
}
fn render_active_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
let theme = theme::get();
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.ui.border));
let inner = block.inner(area);
frame.render_widget(block, area);
let patterns: Vec<(usize, usize, usize)> = snapshot
.active_patterns
.iter()
.map(|p| (p.bank, p.pattern, p.iter))
.collect();
let step_info = snapshot
.get_step(app.editor_ctx.bank, app.editor_ctx.pattern)
.map(|step| (step, app.current_edit_pattern().length));
let mut widget = ActivePatterns::new(&patterns);
if let Some((step, total)) = step_info {
widget = widget.with_step(step, total);
}
frame.render_widget(widget, inner);
}

View File

@@ -3,6 +3,7 @@ use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
#[cfg(not(feature = "desktop"))]
use tui_big_text::{BigText, PixelSize};
use crate::state::ui::UiState;
@@ -16,13 +17,21 @@ pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
let link_style = Style::new().fg(theme.title.link);
let license_style = Style::new().fg(theme.title.license);
#[cfg(not(feature = "desktop"))]
let big_title = BigText::builder()
.pixel_size(PixelSize::Quadrant)
.pixel_size(PixelSize::Full)
.style(Style::new().fg(theme.title.big_title).bold())
.lines(vec!["CAGIRE".into()])
.centered()
.build();
#[cfg(feature = "desktop")]
let big_title = Paragraph::new(Line::from(Span::styled(
"CAGIRE",
Style::new().fg(theme.title.big_title).bold(),
)))
.alignment(Alignment::Center);
let version_style = Style::new().fg(theme.title.subtitle);
let subtitle_lines = vec![
@@ -49,21 +58,43 @@ pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
)),
];
let big_text_height = 4;
#[cfg(not(feature = "desktop"))]
let big_text_height = 8;
#[cfg(feature = "desktop")]
let big_text_height = 1;
let min_title_width = 30;
let subtitle_height = subtitle_lines.len() as u16;
let total_height = big_text_height + subtitle_height;
let vertical_padding = area.height.saturating_sub(total_height) / 2;
let [_, title_area, subtitle_area, _] = Layout::vertical([
Constraint::Length(vertical_padding),
Constraint::Length(big_text_height),
Constraint::Length(subtitle_height),
Constraint::Fill(1),
])
.areas(area);
let show_big_title =
area.height >= (big_text_height + subtitle_height) && area.width >= min_title_width;
frame.render_widget(big_title, title_area);
if show_big_title {
let total_height = big_text_height + subtitle_height;
let vertical_padding = area.height.saturating_sub(total_height) / 2;
let subtitle = Paragraph::new(subtitle_lines).alignment(Alignment::Center);
frame.render_widget(subtitle, subtitle_area);
let [_, title_area, subtitle_area, _] = Layout::vertical([
Constraint::Length(vertical_padding),
Constraint::Length(big_text_height),
Constraint::Length(subtitle_height),
Constraint::Fill(1),
])
.areas(area);
frame.render_widget(big_title, title_area);
let subtitle = Paragraph::new(subtitle_lines).alignment(Alignment::Center);
frame.render_widget(subtitle, subtitle_area);
} else {
let vertical_padding = area.height.saturating_sub(subtitle_height) / 2;
let [_, subtitle_area, _] = Layout::vertical([
Constraint::Length(vertical_padding),
Constraint::Length(subtitle_height),
Constraint::Fill(1),
])
.areas(area);
let subtitle = Paragraph::new(subtitle_lines).alignment(Alignment::Center);
frame.render_widget(subtitle, subtitle_area);
}
}

View File

@@ -1,4 +1,4 @@
pub use cagire_ratatui::{
ConfirmModal, FileBrowserModal, ModalFrame, NavMinimap, NavTile, Orientation, SampleBrowser,
Scope, Spectrum, TextInputModal, VuMeter,
ActivePatterns, ConfirmModal, FileBrowserModal, ModalFrame, NavMinimap, NavTile, Orientation,
SampleBrowser, Scope, Spectrum, TextInputModal, VuMeter,
};