Began the Great Widget Refactor of 2024 and introduced custom widgets for buttons, checkboxes, and input boxes. Up next: loading and table widgets
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use crate::ui::utils::{layout_block, style_block_highlight};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect};
|
||||
use ratatui::prelude::{Style, Styled, Text, Widget};
|
||||
use ratatui::widgets::Paragraph;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Button<'a> {
|
||||
title: &'a str,
|
||||
label: Option<&'a str>,
|
||||
icon: Option<&'a str>,
|
||||
style: Style,
|
||||
is_selected: bool,
|
||||
}
|
||||
|
||||
impl<'a> Button<'a> {
|
||||
pub fn title(mut self, title: &'a str) -> Button<'a> {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label(mut self, label: &'a str) -> Button<'a> {
|
||||
self.label = Some(label);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: &'a str) -> Button<'a> {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Button<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, is_selected: bool) -> Button<'a> {
|
||||
self.is_selected = is_selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn render_button_with_icon(&self, area: Rect, buf: &mut Buffer) {
|
||||
let [title_area, icon_area] = Layout::horizontal([
|
||||
Constraint::Length(self.title.len() as u16),
|
||||
Constraint::Percentage(25),
|
||||
])
|
||||
.flex(Flex::SpaceBetween)
|
||||
.margin(1)
|
||||
.areas(area);
|
||||
let style = style_block_highlight(self.is_selected);
|
||||
|
||||
if let Some(icon) = self.icon {
|
||||
layout_block().style(style).render(area, buf);
|
||||
Paragraph::new(Text::from(self.title))
|
||||
.alignment(Alignment::Left)
|
||||
.style(style)
|
||||
.render(title_area, buf);
|
||||
Paragraph::new(Text::from(format!("{icon} ")))
|
||||
.alignment(Alignment::Right)
|
||||
.style(style)
|
||||
.render(icon_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_labeled_button(&self, area: Rect, buf: &mut Buffer) {
|
||||
let [label_area, button_area] =
|
||||
Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area);
|
||||
let label_paragraph = Paragraph::new(Text::from(format!("\n{}: ", self.label.unwrap())))
|
||||
.alignment(Alignment::Right)
|
||||
.primary();
|
||||
|
||||
if self.icon.is_some() {
|
||||
self.render_button_with_icon(button_area, buf);
|
||||
label_paragraph.render(label_area, buf);
|
||||
} else {
|
||||
self.render_button(button_area, buf);
|
||||
label_paragraph.render(label_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_button(&self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new(Text::from(self.title))
|
||||
.block(layout_block())
|
||||
.alignment(Alignment::Center)
|
||||
.style(style_block_highlight(self.is_selected))
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Button<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if self.label.is_some() {
|
||||
self.render_labeled_button(area, buf);
|
||||
} else if self.icon.is_some() {
|
||||
self.render_button_with_icon(area, buf);
|
||||
} else {
|
||||
self.render_button(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Button<'a> {
|
||||
type Item = Button<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use crate::ui::utils::{borderless_block, layout_block, style_block_highlight};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::prelude::Text;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::widgets::{Paragraph, Widget};
|
||||
|
||||
pub struct Checkbox<'a> {
|
||||
label: &'a str,
|
||||
is_checked: bool,
|
||||
is_highlighted: bool,
|
||||
}
|
||||
|
||||
impl<'a> Checkbox<'a> {
|
||||
pub fn new(label: &'a str) -> Checkbox<'a> {
|
||||
Checkbox {
|
||||
label,
|
||||
is_checked: false,
|
||||
is_highlighted: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn checked(mut self, is_checked: bool) -> Checkbox<'a> {
|
||||
self.is_checked = is_checked;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlighted(mut self, is_selected: bool) -> Checkbox<'a> {
|
||||
self.is_highlighted = is_selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn render_checkbox(&self, area: Rect, buf: &mut Buffer) {
|
||||
let check = if self.is_checked { "✔" } else { "" };
|
||||
let [label_area, checkbox_area] =
|
||||
Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area);
|
||||
let checkbox_box_area = Rect {
|
||||
width: 5,
|
||||
..checkbox_area
|
||||
};
|
||||
|
||||
Paragraph::new(Text::from(format!("\n{}: ", self.label)))
|
||||
.block(borderless_block())
|
||||
.alignment(Alignment::Right)
|
||||
.primary()
|
||||
.render(label_area, buf);
|
||||
|
||||
Paragraph::new(Text::from(check))
|
||||
.block(layout_block())
|
||||
.alignment(Alignment::Center)
|
||||
.style(style_block_highlight(self.is_highlighted).bold())
|
||||
.render(checkbox_box_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Checkbox<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_checkbox(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::prelude::Text;
|
||||
use ratatui::style::{Style, Styled, Stylize};
|
||||
use ratatui::widgets::{Block, Paragraph, Widget};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use crate::ui::utils::{borderless_block, layout_block};
|
||||
|
||||
pub struct InputBox<'a> {
|
||||
content: &'a str,
|
||||
offset: usize,
|
||||
style: Style,
|
||||
block: Block<'a>,
|
||||
label: Option<&'a str>,
|
||||
cursor_after_string: bool,
|
||||
is_highlighted: Option<bool>,
|
||||
is_selected: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'a> InputBox<'a> {
|
||||
pub fn new(content: &'a str) -> InputBox<'_> {
|
||||
InputBox {
|
||||
content,
|
||||
offset: 0,
|
||||
style: Style::new().default(),
|
||||
block: layout_block(),
|
||||
label: None,
|
||||
cursor_after_string: true,
|
||||
is_highlighted: None,
|
||||
is_selected: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> InputBox<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> InputBox<'a> {
|
||||
self.block = block;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label(mut self, label: &'a str) -> InputBox<'a> {
|
||||
self.label = Some(label);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn offset(mut self, offset: usize) -> InputBox<'a> {
|
||||
self.offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cursor_after_string(mut self, cursor_after_string: bool) -> InputBox<'a> {
|
||||
self.cursor_after_string = cursor_after_string;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlighted(mut self, is_highlighted: bool) -> InputBox<'a> {
|
||||
self.is_highlighted = Some(is_highlighted);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, is_selected: bool) -> InputBox<'a> {
|
||||
self.is_selected = Some(is_selected);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_selected(&self) -> bool {
|
||||
self.is_selected.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn show_cursor(&self, f: &mut Frame<'_>, area: Rect) {
|
||||
let area = if self.label.is_some() {
|
||||
Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).split(area)[1]
|
||||
} else {
|
||||
area
|
||||
};
|
||||
|
||||
if self.cursor_after_string {
|
||||
f.set_cursor(
|
||||
area.x + (self.content.len() - self.offset) as u16 + 1,
|
||||
area.y + 1,
|
||||
);
|
||||
} else {
|
||||
f.set_cursor(area.x + 1u16, area.y + 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_input_box(&self, area: Rect, buf: &mut Buffer) {
|
||||
let style =
|
||||
if matches!(self.is_highlighted, Some(true)) && matches!(self.is_selected, Some(false)) {
|
||||
Style::new().system_function().bold()
|
||||
} else {
|
||||
self.style
|
||||
};
|
||||
|
||||
let input_box_paragraph = Paragraph::new(Text::from(self.content))
|
||||
.style(style)
|
||||
.block(self.block.clone());
|
||||
|
||||
if let Some(label) = self.label {
|
||||
let [label_area, text_box_area] =
|
||||
Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area);
|
||||
|
||||
Paragraph::new(Text::from(format!("\n{label}: ")))
|
||||
.block(borderless_block())
|
||||
.alignment(Alignment::Right)
|
||||
.primary()
|
||||
.render(label_area, buf);
|
||||
input_box_paragraph.render(text_box_area, buf);
|
||||
} else {
|
||||
input_box_paragraph.render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for InputBox<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.render_input_box(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for InputBox<'a> {
|
||||
type Item = InputBox<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! render_selectable_input_box {
|
||||
($input_box:ident, $frame:ident, $area:ident) => {
|
||||
if $input_box.is_selected() {
|
||||
$input_box.show_cursor($frame, $area);
|
||||
}
|
||||
|
||||
$frame.render_widget($input_box, $area);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(super) mod button;
|
||||
pub(super) mod input_box;
|
||||
pub(super) mod checkbox;
|
||||
Reference in New Issue
Block a user