use super::*;
impl Context {
pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
slt_assert(cols > 0, "grid() requires at least 1 column");
let interaction_id = self.next_interaction_id();
let border = self.theme.border;
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Column,
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(border),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 0,
group_name: None,
})));
let children_start = self.commands.len();
f(self);
let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
let elements = collect_grid_elements(child_commands);
let cols = cols.max(1) as usize;
for row in elements.chunks(cols) {
self.skip_interaction_slot();
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Row,
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(border),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 0,
group_name: None,
})));
for element in row {
self.skip_interaction_slot();
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Column,
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(border),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 1,
group_name: None,
})));
self.commands.extend(element.iter().cloned());
self.commands.push(Command::EndContainer);
}
self.commands.push(Command::EndContainer);
}
self.commands.push(Command::EndContainer);
self.rollback.last_text_idx = None;
self.response_for(interaction_id)
}
pub fn grid_with(&mut self, columns: &[GridColumn], f: impl FnOnce(&mut Context)) -> Response {
let cols = columns.len().max(1);
let interaction_id = self.next_interaction_id();
let border = self.theme.border;
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Column,
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(border),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 0,
group_name: None,
})));
let children_start = self.commands.len();
f(self);
let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
let elements = collect_grid_elements(child_commands);
for row in elements.chunks(cols) {
self.skip_interaction_slot();
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Row,
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(border),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 0,
group_name: None,
})));
for (col_idx, element) in row.iter().enumerate() {
let spec = columns.get(col_idx).copied().unwrap_or(GridColumn::Auto);
let (grow, constraints) = match spec {
GridColumn::Auto => (1, Constraints::default()),
GridColumn::Fixed(w) => (
0,
Constraints {
min_width: Some(w),
max_width: Some(w),
..Constraints::default()
},
),
GridColumn::Grow(g) => (g, Constraints::default()),
GridColumn::Percent(p) => (
0,
Constraints {
width_pct: Some(p),
..Constraints::default()
},
),
};
self.skip_interaction_slot();
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Column,
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(border),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints,
title: None,
grow,
group_name: None,
})));
self.commands.extend(element.iter().cloned());
self.commands.push(Command::EndContainer);
}
self.commands.push(Command::EndContainer);
}
self.commands.push(Command::EndContainer);
self.rollback.last_text_idx = None;
self.response_for(interaction_id)
}
pub fn list(&mut self, state: &mut ListState) -> Response {
let colors = self.widget_theme.list;
self.list_colored(state, &colors)
}
pub fn list_colored(&mut self, state: &mut ListState, colors: &WidgetColors) -> Response {
let visible = state.visible_indices().to_vec();
if visible.is_empty() && state.items.is_empty() {
state.selected = 0;
return Response::none();
}
if !visible.is_empty() {
state.selected = state.selected.min(visible.len().saturating_sub(1));
}
let old_selected = state.selected;
let focused = self.register_focusable();
let (interaction_id, mut response) = self.begin_widget_interaction(focused);
if focused {
let mut consumed_indices = Vec::new();
for (i, key) in self.available_key_presses() {
match key.code {
KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
let _ = handle_vertical_nav(
&mut state.selected,
visible.len().saturating_sub(1),
key.code.clone(),
);
consumed_indices.push(i);
}
_ => {}
}
}
self.consume_indices(consumed_indices);
}
if let Some((rect, clicks)) = self.left_clicks_for_interaction(interaction_id) {
let mut consumed = Vec::new();
for (i, mouse) in clicks {
let clicked_idx = (mouse.y - rect.y) as usize;
if clicked_idx < visible.len() {
state.selected = clicked_idx;
consumed.push(i);
}
}
self.consume_indices(consumed);
}
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Column,
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 0,
group_name: None,
})));
for (view_idx, &item_idx) in visible.iter().enumerate() {
let item = &state.items[item_idx];
if view_idx == state.selected {
let mut selected_style = Style::new()
.bg(colors.accent.unwrap_or(self.theme.selected_bg))
.fg(colors.fg.unwrap_or(self.theme.selected_fg));
if focused {
selected_style = selected_style.bold();
}
let mut row = String::with_capacity(2 + item.len());
row.push_str("▸ ");
row.push_str(item);
self.styled(row, selected_style);
} else {
let mut row = String::with_capacity(2 + item.len());
row.push_str(" ");
row.push_str(item);
self.styled(row, Style::new().fg(colors.fg.unwrap_or(self.theme.text)));
}
}
self.commands.push(Command::EndContainer);
self.rollback.last_text_idx = None;
response.changed = state.selected != old_selected;
response
}
pub fn calendar(&mut self, state: &mut CalendarState) -> Response {
let focused = self.register_focusable();
let (interaction_id, mut response) = self.begin_widget_interaction(focused);
let month_days = CalendarState::days_in_month(state.year, state.month);
state.cursor_day = state.cursor_day.clamp(1, month_days);
if let Some(day) = state.selected_day {
state.selected_day = Some(day.min(month_days));
}
let old_selected = state.selected_day;
if focused {
let mut consumed_indices = Vec::new();
for (i, key) in self.available_key_presses() {
match key.code {
KeyCode::Left => {
calendar_move_cursor_by_days(state, -1);
consumed_indices.push(i);
}
KeyCode::Right => {
calendar_move_cursor_by_days(state, 1);
consumed_indices.push(i);
}
KeyCode::Up => {
calendar_move_cursor_by_days(state, -7);
consumed_indices.push(i);
}
KeyCode::Down => {
calendar_move_cursor_by_days(state, 7);
consumed_indices.push(i);
}
KeyCode::Char('h') => {
state.prev_month();
consumed_indices.push(i);
}
KeyCode::Char('l') => {
state.next_month();
consumed_indices.push(i);
}
KeyCode::Enter | KeyCode::Char(' ') => {
state.selected_day = Some(state.cursor_day);
consumed_indices.push(i);
}
_ => {}
}
}
self.consume_indices(consumed_indices);
}
if let Some((rect, clicks)) = self.left_clicks_for_interaction(interaction_id) {
let mut consumed = Vec::new();
for (i, mouse) in clicks {
let rel_x = mouse.x.saturating_sub(rect.x);
let rel_y = mouse.y.saturating_sub(rect.y);
if rel_y == 0 {
if rel_x <= 2 {
state.prev_month();
consumed.push(i);
continue;
}
if rel_x + 3 >= rect.width {
state.next_month();
consumed.push(i);
continue;
}
}
if !(2..8).contains(&rel_y) {
continue;
}
if rel_x >= 21 {
continue;
}
let week = rel_y - 2;
let col = rel_x / 3;
let day_index = week * 7 + col;
let first = CalendarState::first_weekday(state.year, state.month);
let days = CalendarState::days_in_month(state.year, state.month);
if day_index < first {
continue;
}
let day = day_index - first + 1;
if day == 0 || day > days {
continue;
}
state.cursor_day = day;
state.selected_day = Some(day);
consumed.push(i);
}
self.consume_indices(consumed);
}
let title = {
let month_name = calendar_month_name(state.month);
let mut s = String::with_capacity(16);
s.push_str(&state.year.to_string());
s.push(' ');
s.push_str(month_name);
s
};
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Column,
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(self.theme.border),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 0,
group_name: None,
})));
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Row,
gap: 1,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(self.theme.border),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 0,
group_name: None,
})));
self.styled("◀", Style::new().fg(self.theme.text));
self.styled(title, Style::new().bold().fg(self.theme.text));
self.styled("▶", Style::new().fg(self.theme.text));
self.commands.push(Command::EndContainer);
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Row,
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(self.theme.border),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 0,
group_name: None,
})));
for wd in ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] {
self.styled(
format!("{wd:>2} "),
Style::new().fg(self.theme.text_dim).bold(),
);
}
self.commands.push(Command::EndContainer);
let first = CalendarState::first_weekday(state.year, state.month);
let days = CalendarState::days_in_month(state.year, state.month);
for week in 0..6_u32 {
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Row,
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(self.theme.border),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 0,
group_name: None,
})));
for col in 0..7_u32 {
let idx = week * 7 + col;
if idx < first || idx >= first + days {
self.styled(" ", Style::new().fg(self.theme.text_dim));
continue;
}
let day = idx - first + 1;
let text = format!("{day:>2} ");
let style = if state.selected_day == Some(day) {
Style::new()
.bg(self.theme.selected_bg)
.fg(self.theme.selected_fg)
} else if state.cursor_day == day {
Style::new().fg(self.theme.primary).bold()
} else {
Style::new().fg(self.theme.text)
};
self.styled(text, style);
}
self.commands.push(Command::EndContainer);
}
self.commands.push(Command::EndContainer);
self.rollback.last_text_idx = None;
response.changed = state.selected_day != old_selected;
response
}
pub fn file_picker(&mut self, state: &mut FilePickerState) -> Response {
if state.dirty {
state.refresh();
}
if !state.entries.is_empty() {
state.selected = state.selected.min(state.entries.len().saturating_sub(1));
}
let focused = self.register_focusable();
let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
let mut file_selected = false;
if focused {
let mut consumed_indices = Vec::new();
for (i, key) in self.available_key_presses() {
match key.code {
KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
if !state.entries.is_empty() {
let _ = handle_vertical_nav(
&mut state.selected,
state.entries.len().saturating_sub(1),
key.code.clone(),
);
}
consumed_indices.push(i);
}
KeyCode::Enter => {
if let Some(entry) = state.entries.get(state.selected).cloned() {
if entry.is_dir {
state.current_dir = entry.path;
state.selected = 0;
state.selected_file = None;
state.dirty = true;
} else {
state.selected_file = Some(entry.path);
file_selected = true;
}
}
consumed_indices.push(i);
}
KeyCode::Backspace => {
if let Some(parent) = state.current_dir.parent().map(|p| p.to_path_buf()) {
state.current_dir = parent;
state.selected = 0;
state.selected_file = None;
state.dirty = true;
}
consumed_indices.push(i);
}
KeyCode::Char('h') => {
state.show_hidden = !state.show_hidden;
state.selected = 0;
state.dirty = true;
consumed_indices.push(i);
}
KeyCode::Esc => {
state.selected_file = None;
consumed_indices.push(i);
}
_ => {}
}
}
self.consume_indices(consumed_indices);
}
if state.dirty {
state.refresh();
}
self.commands
.push(Command::BeginContainer(Box::new(BeginContainerArgs {
direction: Direction::Column,
gap: 0,
align: Align::Start,
align_self: None,
justify: Justify::Start,
border: None,
border_sides: BorderSides::all(),
border_style: Style::new().fg(self.theme.border),
bg_color: None,
padding: Padding::default(),
margin: Margin::default(),
constraints: Constraints::default(),
title: None,
grow: 0,
group_name: None,
})));
let dir_text = {
let dir = state.current_dir.display().to_string();
let mut text = String::with_capacity(5 + dir.len());
text.push_str("Dir: ");
text.push_str(&dir);
text
};
self.styled(dir_text, Style::new().fg(self.theme.text_dim).dim());
if state.entries.is_empty() {
self.styled("(empty)", Style::new().fg(self.theme.text_dim).dim());
} else {
for (idx, entry) in state.entries.iter().enumerate() {
let icon = if entry.is_dir { "▸ " } else { " " };
let row = if entry.is_dir {
let mut row = String::with_capacity(icon.len() + entry.name.len());
row.push_str(icon);
row.push_str(&entry.name);
row
} else {
let size_text = entry.size.to_string();
let mut row =
String::with_capacity(icon.len() + entry.name.len() + size_text.len() + 4);
row.push_str(icon);
row.push_str(&entry.name);
row.push_str(" ");
row.push_str(&size_text);
row.push_str(" B");
row
};
let style = if idx == state.selected {
if focused {
Style::new().bold().fg(self.theme.primary)
} else {
Style::new().fg(self.theme.primary)
}
} else {
Style::new().fg(self.theme.text)
};
self.styled(row, style);
}
}
self.commands.push(Command::EndContainer);
self.rollback.last_text_idx = None;
response.changed = file_selected;
response
}
}
fn collect_grid_elements(child_commands: Vec<Command>) -> Vec<Vec<Command>> {
let mut elements: Vec<Vec<Command>> = Vec::new();
let mut iter = child_commands.into_iter().peekable();
let mut pending_markers: Vec<Command> = Vec::new();
while let Some(cmd) = iter.next() {
match cmd {
Command::InteractionMarker(_) => {
pending_markers.push(cmd);
}
Command::BeginContainer(_) | Command::BeginScrollable(_) => {
let mut depth = 1_u32;
let mut element: Vec<Command> = std::mem::take(&mut pending_markers);
element.push(cmd);
for next in iter.by_ref() {
match next {
Command::BeginContainer(_) | Command::BeginScrollable(_) => {
depth += 1;
}
Command::EndContainer => {
depth = depth.saturating_sub(1);
}
_ => {}
}
let at_end = matches!(next, Command::EndContainer) && depth == 0;
element.push(next);
if at_end {
break;
}
}
elements.push(element);
}
Command::EndContainer => {}
_ => {
let mut element = std::mem::take(&mut pending_markers);
element.push(cmd);
elements.push(element);
}
}
}
if !pending_markers.is_empty() {
elements.push(pending_markers);
}
elements
}