use crate::_private::NonExhaustive;
use crate::layout::GenericLayout;
use event::FormOutcome;
use rat_event::util::MouseFlagsN;
use rat_event::{ConsumedEvent, HandleEvent, MouseOnly, Regular, ct_event};
use rat_focus::{Focus, FocusBuilder, FocusFlag, HasFocus};
use rat_reloc::RelocatableState;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::{Alignment, Rect, Size};
use ratatui_core::style::Style;
use ratatui_core::text::{Line, Span};
use ratatui_core::widgets::{StatefulWidget, Widget};
use ratatui_crossterm::crossterm::event::Event;
use ratatui_widgets::block::{Block, BlockExt};
use std::borrow::Cow;
use std::cmp::min;
use std::hash::Hash;
use std::rc::Rc;
use unicode_display_width::width as unicode_width;
#[derive(Debug, Clone)]
pub struct Form<'a, W = usize>
where
W: Eq + Hash + Clone,
{
layout: Option<GenericLayout<W>>,
style: Style,
block: Option<Block<'a>>,
nav_style: Option<Style>,
nav_hover_style: Option<Style>,
title_style: Option<Style>,
navigation: bool,
next_page: &'a str,
prev_page: &'a str,
first_page: &'a str,
last_page: &'a str,
auto_label: bool,
label_style: Option<Style>,
label_alignment: Option<Alignment>,
}
#[derive(Debug)]
#[must_use]
pub struct FormBuffer<'b, W>
where
W: Eq + Hash + Clone,
{
layout: Rc<GenericLayout<W>>,
page_area: Rect,
widget_area: Rect,
buffer: &'b mut Buffer,
auto_label: bool,
label_style: Option<Style>,
label_alignment: Option<Alignment>,
}
#[derive(Debug, Clone)]
pub struct FormStyle {
pub style: Style,
pub block: Option<Block<'static>>,
pub border_style: Option<Style>,
pub title_style: Option<Style>,
pub label_style: Option<Style>,
pub label_alignment: Option<Alignment>,
pub navigation: Option<Style>,
pub navigation_hover: Option<Style>,
pub show_navigation: Option<bool>,
pub title: Option<Style>,
pub next_page_mark: Option<&'static str>,
pub prev_page_mark: Option<&'static str>,
pub first_page_mark: Option<&'static str>,
pub last_page_mark: Option<&'static str>,
pub non_exhaustive: NonExhaustive,
}
#[derive(Debug, Clone)]
pub struct FormState<W = usize>
where
W: Eq + Hash + Clone,
{
pub layout: Rc<GenericLayout<W>>,
pub area: Rect,
pub widget_area: Rect,
pub prev_area: Rect,
pub next_area: Rect,
pub page: usize,
pub container: FocusFlag,
pub mouse: MouseFlagsN,
pub non_exhaustive: NonExhaustive,
}
pub(crate) mod event {
use rat_event::{ConsumedEvent, Outcome};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FormOutcome {
Continue,
Unchanged,
Changed,
Page,
}
impl ConsumedEvent for FormOutcome {
fn is_consumed(&self) -> bool {
*self != FormOutcome::Continue
}
}
impl From<Outcome> for FormOutcome {
fn from(value: Outcome) -> Self {
match value {
Outcome::Continue => FormOutcome::Continue,
Outcome::Unchanged => FormOutcome::Unchanged,
Outcome::Changed => FormOutcome::Changed,
}
}
}
impl From<FormOutcome> for Outcome {
fn from(value: FormOutcome) -> Self {
match value {
FormOutcome::Continue => Outcome::Continue,
FormOutcome::Unchanged => Outcome::Unchanged,
FormOutcome::Changed => Outcome::Changed,
FormOutcome::Page => Outcome::Changed,
}
}
}
}
impl<W> Default for Form<'_, W>
where
W: Eq + Hash + Clone,
{
fn default() -> Self {
Self {
layout: Default::default(),
style: Default::default(),
block: Default::default(),
nav_style: Default::default(),
nav_hover_style: Default::default(),
title_style: Default::default(),
navigation: true,
next_page: ">>>",
prev_page: "<<<",
first_page: " [ ",
last_page: " ] ",
auto_label: true,
label_style: Default::default(),
label_alignment: Default::default(),
}
}
}
impl<'a, W> Form<'a, W>
where
W: Eq + Hash + Clone,
{
pub fn new() -> Self {
Self::default()
}
pub fn layout(mut self, layout: GenericLayout<W>) -> Self {
self.layout = Some(layout);
self
}
pub fn set_layout(&mut self, layout: GenericLayout<W>) {
self.layout = Some(layout);
}
pub fn auto_label(mut self, auto: bool) -> Self {
self.auto_label = auto;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self.block = self.block.map(|v| v.style(style));
self
}
pub fn nav_style(mut self, nav_style: Style) -> Self {
self.nav_style = Some(nav_style);
self
}
pub fn nav_hover_style(mut self, nav_hover: Style) -> Self {
self.nav_hover_style = Some(nav_hover);
self
}
pub fn show_navigation(mut self, show: bool) -> Self {
self.navigation = show;
self
}
pub fn title_style(mut self, title_style: Style) -> Self {
self.title_style = Some(title_style);
self
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block.style(self.style));
self
}
pub fn next_page_mark(mut self, txt: &'a str) -> Self {
self.next_page = txt;
self
}
pub fn prev_page_mark(mut self, txt: &'a str) -> Self {
self.prev_page = txt;
self
}
pub fn first_page_mark(mut self, txt: &'a str) -> Self {
self.first_page = txt;
self
}
pub fn last_page_mark(mut self, txt: &'a str) -> Self {
self.last_page = txt;
self
}
pub fn label_style(mut self, style: Style) -> Self {
self.label_style = Some(style);
self
}
pub fn label_alignment(mut self, alignment: Alignment) -> Self {
self.label_alignment = Some(alignment);
self
}
pub fn styles(mut self, styles: FormStyle) -> Self {
self.style = styles.style;
if styles.block.is_some() {
self.block = styles.block;
}
if let Some(border_style) = styles.border_style {
self.block = self.block.map(|v| v.border_style(border_style));
}
if let Some(title_style) = styles.title_style {
self.block = self.block.map(|v| v.title_style(title_style));
}
self.block = self.block.map(|v| v.style(self.style));
if let Some(nav) = styles.navigation {
self.nav_style = Some(nav);
}
if let Some(nav) = styles.navigation_hover {
self.nav_hover_style = Some(nav);
}
if let Some(navigation) = styles.show_navigation {
self.navigation = navigation;
}
if let Some(title) = styles.title {
self.title_style = Some(title);
}
if let Some(txt) = styles.next_page_mark {
self.next_page = txt;
}
if let Some(txt) = styles.prev_page_mark {
self.prev_page = txt;
}
if let Some(txt) = styles.first_page_mark {
self.first_page = txt;
}
if let Some(txt) = styles.last_page_mark {
self.last_page = txt;
}
if let Some(label) = styles.label_style {
self.label_style = Some(label);
}
if let Some(alignment) = styles.label_alignment {
self.label_alignment = Some(alignment);
}
self
}
pub fn layout_size(&self, area: Rect) -> Size {
self.block.inner_if_some(area).as_size()
}
pub fn layout_area(&self, area: Rect) -> Rect {
if let Some(block) = &self.block
&& self.navigation
{
block.inner(area)
} else {
area
}
}
#[allow(clippy::needless_lifetimes)]
pub fn into_buffer<'b, 's>(
mut self,
area: Rect,
buf: &'b mut Buffer,
state: &'s mut FormState<W>,
) -> FormBuffer<'b, W> {
state.area = area;
state.widget_area = self.layout_area(area);
if let Some(layout) = self.layout.take() {
state.layout = Rc::new(layout);
}
let page_size = state.layout.page_size();
assert!(page_size.height < u16::MAX || page_size.height == u16::MAX && state.page == 0);
let page_area = Rect::new(
0,
(state.page as u16).saturating_mul(page_size.height),
page_size.width,
page_size.height,
);
if self.navigation {
self.render_navigation(area, buf, state);
} else {
buf.set_style(area, self.style);
}
let mut form_buf = FormBuffer {
layout: state.layout.clone(),
page_area,
widget_area: state.widget_area,
buffer: buf,
auto_label: true,
label_style: self.label_style,
label_alignment: self.label_alignment,
};
form_buf.render_block();
form_buf
}
fn render_navigation(&self, area: Rect, buf: &mut Buffer, state: &mut FormState<W>) {
let page_count = state.layout.page_count();
if !state.layout.is_endless() {
if state.page > 0 {
state.prev_area =
Rect::new(area.x, area.y, unicode_width(self.prev_page) as u16, 1);
} else {
state.prev_area =
Rect::new(area.x, area.y, unicode_width(self.first_page) as u16, 1);
}
if (state.page + 1) < page_count {
let p = unicode_width(self.next_page) as u16;
state.next_area = Rect::new(area.x + area.width.saturating_sub(p), area.y, p, 1);
} else {
let p = unicode_width(self.last_page) as u16;
state.next_area = Rect::new(area.x + area.width.saturating_sub(p), area.y, p, 1);
}
} else {
state.prev_area = Default::default();
state.next_area = Default::default();
}
let block = if page_count > 1 {
let title = format!(" {}/{} ", state.page + 1, page_count);
let block = self
.block
.clone()
.unwrap_or_else(|| Block::new().style(self.style))
.title_bottom(title)
.title_alignment(Alignment::Right);
if let Some(title_style) = self.title_style {
block.title_style(title_style)
} else {
block
}
} else {
self.block
.clone()
.unwrap_or_else(|| Block::new().style(self.style))
};
block.render(area, buf);
if !state.layout.is_endless() {
let nav_style = self.nav_style.unwrap_or(self.style);
let nav_hover_style = self.nav_hover_style.unwrap_or(self.style);
if matches!(state.mouse.hover.get(), Some(0)) {
buf.set_style(state.prev_area, nav_hover_style);
} else {
buf.set_style(state.prev_area, nav_style);
}
if state.page > 0 {
Span::from(self.prev_page).render(state.prev_area, buf);
} else {
Span::from(self.first_page).render(state.prev_area, buf);
}
if matches!(state.mouse.hover.get(), Some(1)) {
buf.set_style(state.next_area, nav_hover_style);
} else {
buf.set_style(state.next_area, nav_style);
}
if (state.page + 1) < page_count {
Span::from(self.next_page).render(state.next_area, buf);
} else {
Span::from(self.last_page).render(state.next_area, buf);
}
}
}
}
impl<'b, W> FormBuffer<'b, W>
where
W: Eq + Hash + Clone,
{
pub fn is_visible(&self, widget: W) -> bool {
if let Some(idx) = self.layout.try_index_of(widget) {
self.locate_area(self.layout.widget(idx)).is_some()
} else {
false
}
}
fn render_block(&mut self) {
for (idx, block_area) in self.layout.block_area_iter().enumerate() {
if let Some(block_area) = self.locate_area(*block_area) {
if let Some(block) = self.layout.block(idx) {
block.render(block_area, self.buffer);
}
}
}
}
#[inline(always)]
pub fn render_label<FN>(&mut self, widget: W, render_fn: FN) -> bool
where
FN: FnOnce(&Cow<'static, str>, Rect, &mut Buffer),
{
let Some(idx) = self.layout.try_index_of(widget) else {
return false;
};
let Some(label_area) = self.locate_area(self.layout.label(idx)) else {
return false;
};
if let Some(label_str) = self.layout.try_label_str(idx) {
render_fn(label_str, label_area, self.buffer);
} else {
render_fn(&Cow::default(), label_area, self.buffer);
}
true
}
#[inline(always)]
pub fn render_widget<FN, WW>(&mut self, widget: W, render_fn: FN) -> bool
where
FN: FnOnce() -> WW,
WW: Widget,
{
let Some(idx) = self.layout.try_index_of(widget) else {
return false;
};
if self.auto_label {
self.render_auto_label(idx);
}
let Some(widget_area) = self.locate_area(self.layout.widget(idx)) else {
return false;
};
render_fn().render(widget_area, self.buffer);
true
}
#[inline(always)]
pub fn render<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
where
FN: FnOnce() -> WW,
WW: StatefulWidget<State = SS>,
SS: RelocatableState,
{
let Some(idx) = self.layout.try_index_of(widget) else {
return false;
};
if self.auto_label {
self.render_auto_label(idx);
}
let Some(widget_area) = self.locate_area(self.layout.widget(idx)) else {
state.relocate_hidden();
return false;
};
let widget = render_fn();
widget.render(widget_area, self.buffer, state);
true
}
#[inline(always)]
#[allow(clippy::question_mark)]
pub fn render2<FN, WW, SS, R>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> Option<R>
where
FN: FnOnce() -> (WW, R),
WW: StatefulWidget<State = SS>,
SS: RelocatableState,
{
let Some(idx) = self.layout.try_index_of(widget) else {
return None;
};
if self.auto_label {
self.render_auto_label(idx);
}
let Some(widget_area) = self.locate_area(self.layout.widget(idx)) else {
state.relocate_hidden();
return None;
};
let (widget, remainder) = render_fn();
widget.render(widget_area, self.buffer, state);
Some(remainder)
}
#[inline(always)]
#[deprecated(since = "2.3.0", note = "use render_popup() for popups")]
pub fn render_opt<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
where
FN: FnOnce() -> Option<WW>,
WW: StatefulWidget<State = SS>,
SS: RelocatableState,
{
let Some(idx) = self.layout.try_index_of(widget) else {
return false;
};
if self.auto_label {
self.render_auto_label(idx);
}
let Some(widget_area) = self.locate_area(self.layout.widget(idx)) else {
state.relocate_hidden();
return false;
};
let widget = render_fn();
if let Some(widget) = widget {
widget.render(widget_area, self.buffer, state);
true
} else {
state.relocate_hidden();
false
}
}
#[inline(always)]
pub fn render_popup<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
where
FN: FnOnce() -> Option<WW>,
WW: StatefulWidget<State = SS>,
SS: RelocatableState,
{
let Some(idx) = self.layout.try_index_of(widget) else {
return false;
};
let Some(widget_area) = self.locate_area(self.layout.widget(idx)) else {
state.relocate_popup_hidden();
return false;
};
let widget = render_fn();
if let Some(widget) = widget {
widget.render(widget_area, self.buffer, state);
true
} else {
state.relocate_popup_hidden();
false
}
}
pub fn buffer(&mut self) -> &mut Buffer {
self.buffer
}
#[inline(always)]
fn render_auto_label(&mut self, idx: usize) -> bool {
let Some(label_area) = self.locate_area(self.layout.label(idx)) else {
return false;
};
let Some(label_str) = self.layout.try_label_str(idx) else {
return false;
};
let mut label = Line::from(label_str.as_ref());
if let Some(style) = self.label_style {
label = label.style(style)
};
if let Some(align) = self.label_alignment {
label = label.alignment(align);
}
label.render(label_area, self.buffer);
true
}
pub fn locate_widget(&self, widget: W) -> Option<Rect> {
let Some(idx) = self.layout.try_index_of(widget) else {
return None;
};
self.locate_area(self.layout.widget(idx))
}
pub fn locate_label(&self, widget: W) -> Option<Rect> {
let Some(idx) = self.layout.try_index_of(widget) else {
return None;
};
self.locate_area(self.layout.label(idx))
}
#[inline]
pub fn locate_area(&self, area: Rect) -> Option<Rect> {
let area = self.page_area.intersection(area);
if self.page_area.intersects(area) {
let located = Rect::new(
area.x - self.page_area.x + self.widget_area.x,
area.y - self.page_area.y + self.widget_area.y,
area.width,
area.height,
);
let located = self.widget_area.intersection(located);
if self.widget_area.intersects(located) {
Some(located)
} else {
None
}
} else {
None
}
}
}
impl Default for FormStyle {
fn default() -> Self {
Self {
style: Default::default(),
label_style: Default::default(),
label_alignment: Default::default(),
navigation: Default::default(),
navigation_hover: Default::default(),
show_navigation: Default::default(),
title: Default::default(),
block: Default::default(),
border_style: Default::default(),
title_style: Default::default(),
next_page_mark: Default::default(),
prev_page_mark: Default::default(),
first_page_mark: Default::default(),
last_page_mark: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl<W> Default for FormState<W>
where
W: Eq + Hash + Clone,
{
fn default() -> Self {
Self {
layout: Default::default(),
area: Default::default(),
widget_area: Default::default(),
prev_area: Default::default(),
next_area: Default::default(),
page: 0,
container: Default::default(),
mouse: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl<W> HasFocus for FormState<W>
where
W: Eq + Hash + Clone,
{
fn build(&self, _builder: &mut FocusBuilder) {
}
fn focus(&self) -> FocusFlag {
self.container.clone()
}
fn area(&self) -> Rect {
self.area
}
}
impl<W> RelocatableState for FormState<W>
where
W: Eq + Hash + Clone,
{
fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
self.area.relocate(shift, clip);
self.widget_area.relocate(shift, clip);
self.prev_area.relocate(shift, clip);
self.next_area.relocate(shift, clip);
}
}
impl<W> FormState<W>
where
W: Eq + Hash + Clone,
{
pub fn new() -> Self {
Self::default()
}
pub fn named(name: &str) -> Self {
let mut z = Self::default();
z.container = z.container.with_name(name);
z
}
pub fn clear(&mut self) {
self.layout = Default::default();
self.page = 0;
}
pub fn valid_layout(&self, size: Size) -> bool {
!self.layout.size_changed(size) && !self.layout.is_empty()
}
pub fn set_layout(&mut self, layout: GenericLayout<W>) {
self.layout = Rc::new(layout);
}
pub fn layout(&self) -> Rc<GenericLayout<W>> {
self.layout.clone()
}
pub fn show(&mut self, widget: W) {
let page = self.layout.page_of(widget).unwrap_or_default();
self.set_page(page);
}
pub fn page_count(&self) -> usize {
self.layout.page_count()
}
pub fn first(&self, page: usize) -> Option<W> {
self.layout.first(page)
}
pub fn page_of(&self, widget: W) -> Option<usize> {
self.layout.page_of(widget)
}
pub fn set_page(&mut self, page: usize) -> bool {
let old_page = self.page;
self.page = min(page, self.page_count().saturating_sub(1));
old_page != self.page
}
pub fn page(&self) -> usize {
self.page
}
pub fn next_page(&mut self) -> bool {
let old_page = self.page;
if self.page + 1 == self.page_count() {
} else if self.page + 1 > self.page_count() {
self.page = self.page_count().saturating_sub(1);
} else {
self.page += 1;
}
old_page != self.page
}
pub fn prev_page(&mut self) -> bool {
if self.page >= 1 {
self.page -= 1;
true
} else if self.page > 0 {
self.page = 0;
true
} else {
false
}
}
}
impl FormState<usize> {
pub fn focus_first(&self, focus: &Focus) -> bool {
if let Some(w) = self.first(self.page) {
focus.by_widget_id(w);
true
} else {
false
}
}
pub fn show_focused(&mut self, focus: &Focus) -> bool {
let Some(focused) = focus.focused() else {
return false;
};
let focused = focused.widget_id();
let page = self.layout.page_of(focused);
if let Some(page) = page {
self.set_page(page);
true
} else {
false
}
}
}
impl FormState<FocusFlag> {
pub fn focus_first(&self, focus: &Focus) -> bool {
if let Some(w) = self.first(self.page) {
focus.focus(&w);
true
} else {
false
}
}
pub fn show_focused(&mut self, focus: &Focus) -> bool {
let Some(focused) = focus.focused() else {
return false;
};
let page = self.layout.page_of(focused);
if let Some(page) = page {
self.set_page(page);
true
} else {
false
}
}
}
impl<W> HandleEvent<Event, Regular, FormOutcome> for FormState<W>
where
W: Eq + Hash + Clone,
{
fn handle(&mut self, event: &Event, _qualifier: Regular) -> FormOutcome {
let r = if self.container.is_focused() && !self.layout.is_endless() {
match event {
ct_event!(keycode press ALT-PageUp) => {
if self.prev_page() {
FormOutcome::Page
} else {
FormOutcome::Continue
}
}
ct_event!(keycode press ALT-PageDown) => {
if self.next_page() {
FormOutcome::Page
} else {
FormOutcome::Continue
}
}
_ => FormOutcome::Continue,
}
} else {
FormOutcome::Continue
};
r.or_else(|| self.handle(event, MouseOnly))
}
}
impl<W> HandleEvent<Event, MouseOnly, FormOutcome> for FormState<W>
where
W: Eq + Hash + Clone,
{
fn handle(&mut self, event: &Event, _qualifier: MouseOnly) -> FormOutcome {
if !self.layout.is_endless() {
match event {
ct_event!(mouse down Left for x,y) if self.prev_area.contains((*x, *y).into()) => {
if self.prev_page() {
FormOutcome::Page
} else {
FormOutcome::Unchanged
}
}
ct_event!(mouse down Left for x,y) if self.next_area.contains((*x, *y).into()) => {
if self.next_page() {
FormOutcome::Page
} else {
FormOutcome::Unchanged
}
}
ct_event!(scroll down for x,y) => {
if self.area.contains((*x, *y).into()) {
if self.next_page() {
FormOutcome::Page
} else {
FormOutcome::Continue
}
} else {
FormOutcome::Continue
}
}
ct_event!(scroll up for x,y) => {
if self.area.contains((*x, *y).into()) {
if self.prev_page() {
FormOutcome::Page
} else {
FormOutcome::Continue
}
} else {
FormOutcome::Continue
}
}
ct_event!(mouse any for m)
if self.mouse.hover(&[self.prev_area, self.next_area], m) =>
{
FormOutcome::Changed
}
_ => FormOutcome::Continue,
}
} else {
FormOutcome::Continue
}
}
}
pub fn handle_events<W>(state: &mut FormState<W>, _focus: bool, event: &Event) -> FormOutcome
where
W: Eq + Clone + Hash,
{
HandleEvent::handle(state, event, Regular)
}
pub fn handle_mouse_events<W>(state: &mut FormState<W>, event: &Event) -> FormOutcome
where
W: Eq + Clone + Hash,
{
HandleEvent::handle(state, event, MouseOnly)
}