This commit is contained in:
Alex 2026-07-25 22:59:25 +02:00
commit 009173a97d
Signed by: alex
SSH key fingerprint: SHA256:D1+Ub8o0v4K5y1JNivW8IxEOelqLSvPmUzBbDIoZkRQ
49 changed files with 1788 additions and 389 deletions

View file

@ -39,8 +39,15 @@ pub fn render_button(
}
};
frame.render_widget(
Paragraph::new(Span::styled(if button.focused { format!(" {}", button.label) } else { button.label.to_owned() }, style))
.alignment(Alignment::Center),
Paragraph::new(Span::styled(
if button.focused {
format!(" {}", button.label)
} else {
button.label.to_owned()
},
style,
))
.alignment(Alignment::Center),
area,
);
}

View file

@ -1,8 +1,8 @@
pub mod action;
pub mod button;
pub mod checkbox_group;
pub mod header;
pub mod choice;
pub mod header;
pub mod navigation;
pub mod panel;
pub mod radio_group;

View file

@ -1,23 +1,60 @@
use crate::theme::{ChromeMode, ResolvedTheme};
use ratatui::{Frame, layout::Rect, widgets::{Block, Borders, Paragraph}};
use ratatui::{
Frame,
layout::Rect,
widgets::{Block, Borders, Paragraph},
};
/// Draw a conventional outlined panel or a filled surface from the same call
/// site. Screens can migrate without embedding theme branches in layouts.
pub fn render_panel(frame: &mut Frame, area: Rect, title: &str, focused: bool, theme: &ResolvedTheme) -> Rect {
pub fn render_panel(
frame: &mut Frame,
area: Rect,
title: &str,
focused: bool,
theme: &ResolvedTheme,
) -> Rect {
match theme.chrome {
ChromeMode::Bordered => {
let block = Block::default().title(title).borders(Borders::ALL).border_style(if focused { theme.borders.focused } else { theme.borders.normal });
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(if focused {
theme.borders.focused
} else {
theme.borders.normal
});
let inner = block.inner(area);
frame.render_widget(block, area);
inner
}
ChromeMode::Surfaces => {
frame.render_widget(Block::default().style(if focused { theme.surfaces.panel_focused } else { theme.surfaces.panel }), area);
let header = Rect { x: area.x, y: area.y, width: area.width, height: area.height.min(1) };
frame.render_widget(Paragraph::new(format!(" {title}")).style(theme.surfaces.panel_alternate), header);
frame.render_widget(
Block::default().style(if focused {
theme.surfaces.panel_focused
} else {
theme.surfaces.panel
}),
area,
);
let header = Rect {
x: area.x,
y: area.y,
width: area.width,
height: area.height.min(1),
};
frame.render_widget(
Paragraph::new(format!(" {title}")).style(theme.surfaces.panel_alternate),
header,
);
// Surface panels use a single header row. A one-cell inset keeps
// compact controls such as the console usable at height three.
Rect { x: area.x.saturating_add(1), y: area.y.saturating_add(1), width: area.width.saturating_sub(2), height: area.height.saturating_sub(1) }
Rect {
x: area.x.saturating_add(1),
y: area.y.saturating_add(1),
width: area.width.saturating_sub(2),
height: area.height.saturating_sub(1),
}
}
}
}

View file

@ -1,20 +1,53 @@
use ratatui::{Frame, layout::Rect, widgets::{Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}};
use ratatui::{
Frame,
layout::Rect,
widgets::{Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
};
/// Reusable viewport policy for long, vertically stacked terminal content.
#[derive(Clone, Copy, Debug)]
pub struct ScrollOptions { pub show_scrollbar: bool, pub render_partial_components: bool }
impl Default for ScrollOptions { fn default() -> Self { Self { show_scrollbar: true, render_partial_components: true } } }
pub struct ScrollOptions {
pub show_scrollbar: bool,
pub render_partial_components: bool,
}
impl Default for ScrollOptions {
fn default() -> Self {
Self {
show_scrollbar: true,
render_partial_components: true,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ScrollField { pub offset: u16, pub options: ScrollOptions }
pub struct ScrollField {
pub offset: u16,
pub options: ScrollOptions,
}
impl ScrollField {
pub fn up(&mut self, amount: u16) { self.offset = self.offset.saturating_sub(amount); }
pub fn down(&mut self, amount: u16, content_height: u16, viewport_height: u16) { self.offset = (self.offset.saturating_add(amount)).min(content_height.saturating_sub(viewport_height)); }
pub fn render(&self, frame: &mut Frame, area: Rect, content: Paragraph<'_>, content_height: u16) {
pub fn up(&mut self, amount: u16) {
self.offset = self.offset.saturating_sub(amount);
}
pub fn down(&mut self, amount: u16, content_height: u16, viewport_height: u16) {
self.offset = (self.offset.saturating_add(amount))
.min(content_height.saturating_sub(viewport_height));
}
pub fn render(
&self,
frame: &mut Frame,
area: Rect,
content: Paragraph<'_>,
content_height: u16,
) {
frame.render_widget(content.scroll((self.offset, 0)), area);
if self.options.show_scrollbar && content_height > area.height {
let mut state = ScrollbarState::new(content_height as usize).position(self.offset as usize);
frame.render_stateful_widget(Scrollbar::new(ScrollbarOrientation::VerticalRight), area, &mut state);
let mut state =
ScrollbarState::new(content_height as usize).position(self.offset as usize);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight),
area,
&mut state,
);
}
}
}