use crate::_private::NonExhaustive;
use crate::choice::core::ChoiceCore;
use crate::event::ChoiceOutcome;
use crate::text::HasScreenCursor;
use crate::util::{block_padding, block_size, revert_style};
use rat_event::util::{MouseFlags, item_at, mouse_trap};
use rat_event::{ConsumedEvent, HandleEvent, MouseOnly, Popup, ct_event};
use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
use rat_popup::event::PopupOutcome;
use rat_popup::{Placement, PopupCore, PopupCoreState, PopupStyle, fallback_popup_style};
use rat_reloc::RelocatableState;
use rat_scrolled::event::ScrollOutcome;
use rat_scrolled::{Scroll, ScrollArea, ScrollAreaState, ScrollState, ScrollStyle};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::{Alignment, Rect};
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::cell::{Cell, RefCell};
use std::cmp::{max, min};
use std::marker::PhantomData;
use std::rc::Rc;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ChoiceFocus {
#[default]
SeparateOpen,
OpenOnFocusGained,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ChoiceSelect {
#[default]
MouseScroll,
MouseMove,
MouseClick,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ChoiceClose {
#[default]
SingleClick,
DoubleClick,
}
#[derive(Debug, Clone)]
pub struct Choice<'a, T = usize>
where
T: PartialEq + Clone + Default,
{
values: Rc<RefCell<Vec<T>>>,
default_value: Option<T>,
unknown_item: Option<Line<'a>>,
items: Rc<RefCell<Vec<Line<'a>>>>,
style: Style,
button_style: Option<Style>,
select_style: Option<Style>,
select_marker: Option<char>,
focus_style: Option<Style>,
block: Option<Block<'a>>,
skip_item_render: bool,
popup_alignment: Alignment,
popup_placement: Placement,
popup_len: Option<u16>,
popup: PopupCore,
popup_style: Style,
popup_scroll: Option<Scroll<'a>>,
popup_block: Option<Block<'a>>,
behave_focus: ChoiceFocus,
behave_select: ChoiceSelect,
behave_close: ChoiceClose,
}
#[derive(Debug)]
pub struct ChoiceWidget<'a, T>
where
T: PartialEq,
{
values: Rc<RefCell<Vec<T>>>,
default_value: Option<T>,
unknown_item: Option<Line<'a>>,
items: Rc<RefCell<Vec<Line<'a>>>>,
style: Style,
button_style: Option<Style>,
focus_style: Option<Style>,
block: Option<Block<'a>>,
skip_item_render: bool,
len: Option<u16>,
behave_focus: ChoiceFocus,
behave_select: ChoiceSelect,
behave_close: ChoiceClose,
}
#[derive(Debug)]
pub struct ChoicePopup<'a, T>
where
T: PartialEq,
{
items: Rc<RefCell<Vec<Line<'a>>>>,
style: Style,
select_style: Option<Style>,
select_marker: Option<char>,
popup_alignment: Alignment,
popup_placement: Placement,
popup_len: Option<u16>,
popup: PopupCore,
popup_style: Style,
popup_scroll: Option<Scroll<'a>>,
popup_block: Option<Block<'a>>,
_phantom: PhantomData<T>,
}
#[derive(Debug, Clone)]
pub struct ChoiceStyle {
pub style: Style,
pub button: Option<Style>,
pub select: Option<Style>,
pub select_marker: Option<char>,
pub focus: Option<Style>,
pub block: Option<Block<'static>>,
pub border_style: Option<Style>,
pub title_style: Option<Style>,
pub popup: PopupStyle,
pub popup_style: Option<Style>,
pub popup_border: Option<Style>,
pub popup_scroll: Option<ScrollStyle>,
pub popup_block: Option<Block<'static>>,
pub popup_len: Option<u16>,
pub behave_focus: Option<ChoiceFocus>,
pub behave_select: Option<ChoiceSelect>,
pub behave_close: Option<ChoiceClose>,
pub non_exhaustive: NonExhaustive,
}
#[derive(Debug)]
pub struct ChoiceState<T = usize>
where
T: PartialEq + Clone + Default,
{
pub area: Rect,
pub inner: Rect,
pub nav_char: Vec<Vec<char>>,
pub item_area: Rect,
pub button_area: Rect,
pub item_areas: Vec<Rect>,
pub core: ChoiceCore<T>,
pub popup: PopupCoreState,
pub popup_scroll: ScrollState,
pub behave_focus: Rc<Cell<ChoiceFocus>>,
pub behave_select: ChoiceSelect,
pub behave_close: ChoiceClose,
pub focus: FocusFlag,
pub mouse: MouseFlags,
pub non_exhaustive: NonExhaustive,
}
pub(crate) mod event {
use rat_event::{ConsumedEvent, Outcome};
use rat_popup::event::PopupOutcome;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChoiceOutcome {
Continue,
Unchanged,
Changed,
Value,
}
impl ConsumedEvent for ChoiceOutcome {
fn is_consumed(&self) -> bool {
*self != ChoiceOutcome::Continue
}
}
impl From<Outcome> for ChoiceOutcome {
fn from(value: Outcome) -> Self {
match value {
Outcome::Continue => ChoiceOutcome::Continue,
Outcome::Unchanged => ChoiceOutcome::Unchanged,
Outcome::Changed => ChoiceOutcome::Changed,
}
}
}
impl From<ChoiceOutcome> for Outcome {
fn from(value: ChoiceOutcome) -> Self {
match value {
ChoiceOutcome::Continue => Outcome::Continue,
ChoiceOutcome::Unchanged => Outcome::Unchanged,
ChoiceOutcome::Changed => Outcome::Changed,
ChoiceOutcome::Value => Outcome::Changed,
}
}
}
impl From<PopupOutcome> for ChoiceOutcome {
fn from(value: PopupOutcome) -> Self {
match value {
PopupOutcome::Continue => ChoiceOutcome::Continue,
PopupOutcome::Unchanged => ChoiceOutcome::Unchanged,
PopupOutcome::Changed => ChoiceOutcome::Changed,
PopupOutcome::Hide => ChoiceOutcome::Changed,
}
}
}
}
pub mod core {
#[derive(Debug, Default, Clone)]
pub struct ChoiceCore<T>
where
T: PartialEq + Clone + Default,
{
values: Vec<T>,
default_value: Option<T>,
value: T,
selected: Option<usize>,
}
impl<T> ChoiceCore<T>
where
T: PartialEq + Clone + Default,
{
pub fn set_values(&mut self, values: Vec<T>) {
self.values = values;
if self.values.is_empty() {
self.selected = None;
} else {
self.selected = self.values.iter().position(|v| *v == self.value);
}
}
pub fn values(&self) -> &[T] {
&self.values
}
pub fn set_default_value(&mut self, default_value: Option<T>) {
self.default_value = default_value.clone();
}
pub fn default_value(&self) -> &Option<T> {
&self.default_value
}
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn set_selected(&mut self, select: usize) -> bool {
let old_sel = self.selected;
if self.values.is_empty() {
self.selected = None;
} else {
if let Some(value) = self.values.get(select) {
self.value = value.clone();
self.selected = Some(select);
} else {
self.selected = None;
}
}
old_sel != self.selected
}
pub fn set_value(&mut self, value: T) -> bool {
let old_value = self.value.clone();
self.value = value;
self.selected = self.values.iter().position(|v| *v == self.value);
old_value != self.value
}
pub fn value(&self) -> T {
self.value.clone()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn clear(&mut self) -> bool {
let old_selected = self.selected;
let old_value = self.value.clone();
if let Some(default_value) = &self.default_value {
self.value = default_value.clone();
}
self.selected = self.values.iter().position(|v| *v == self.value);
old_selected != self.selected || old_value != self.value
}
}
}
impl Default for ChoiceStyle {
fn default() -> Self {
Self {
style: Default::default(),
button: Default::default(),
select: Default::default(),
select_marker: Default::default(),
focus: Default::default(),
block: Default::default(),
border_style: Default::default(),
title_style: Default::default(),
popup: Default::default(),
popup_style: Default::default(),
popup_border: Default::default(),
popup_scroll: Default::default(),
popup_block: Default::default(),
popup_len: Default::default(),
behave_focus: Default::default(),
behave_select: Default::default(),
behave_close: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl<T> Default for Choice<'_, T>
where
T: PartialEq + Clone + Default,
{
fn default() -> Self {
Self {
values: Default::default(),
default_value: Default::default(),
unknown_item: Default::default(),
items: Default::default(),
style: Default::default(),
button_style: Default::default(),
select_style: Default::default(),
select_marker: Default::default(),
focus_style: Default::default(),
block: Default::default(),
popup_len: Default::default(),
popup_alignment: Alignment::Left,
popup_placement: Placement::BelowOrAbove,
popup: Default::default(),
popup_style: Default::default(),
popup_scroll: Default::default(),
popup_block: Default::default(),
behave_focus: Default::default(),
behave_select: Default::default(),
behave_close: Default::default(),
skip_item_render: Default::default(),
}
}
}
impl<'a> Choice<'a, usize> {
#[inline]
pub fn auto_items<V: Into<Line<'a>>>(self, items: impl IntoIterator<Item = V>) -> Self {
{
let mut values = self.values.borrow_mut();
let mut itemz = self.items.borrow_mut();
values.clear();
itemz.clear();
for (k, v) in items.into_iter().enumerate() {
values.push(k);
itemz.push(v.into());
}
}
self
}
pub fn auto_item(self, item: impl Into<Line<'a>>) -> Self {
let idx = self.values.borrow().len();
self.values.borrow_mut().push(idx);
self.items.borrow_mut().push(item.into());
self
}
}
impl<'a, T> Choice<'a, T>
where
T: PartialEq + Clone + Default,
{
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn unknown_item(mut self, unknown: impl Into<Line<'a>>) -> Self {
self.unknown_item = Some(unknown.into());
self
}
#[inline]
pub fn items<V: Into<Line<'a>>>(self, items: impl IntoIterator<Item = (T, V)>) -> Self {
{
let mut values = self.values.borrow_mut();
let mut itemz = self.items.borrow_mut();
values.clear();
itemz.clear();
for (k, v) in items.into_iter() {
values.push(k);
itemz.push(v.into());
}
}
self
}
pub fn item(self, value: T, item: impl Into<Line<'a>>) -> Self {
self.values.borrow_mut().push(value);
self.items.borrow_mut().push(item.into());
self
}
pub fn default_value(mut self, default: T) -> Self {
self.default_value = Some(default);
self
}
pub fn styles(mut self, styles: ChoiceStyle) -> 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 styles.button.is_some() {
self.button_style = styles.button;
}
if styles.select.is_some() {
self.select_style = styles.select;
}
if let Some(marker) = styles.select_marker {
self.select_marker = Some(marker);
}
if styles.focus.is_some() {
self.focus_style = styles.focus;
}
if let Some(select) = styles.behave_focus {
self.behave_focus = select;
}
if let Some(select) = styles.behave_select {
self.behave_select = select;
}
if let Some(close) = styles.behave_close {
self.behave_close = close;
}
if let Some(alignment) = styles.popup.alignment {
self.popup_alignment = alignment;
}
if let Some(placement) = styles.popup.placement {
self.popup_placement = placement;
}
self.popup = self.popup.styles(styles.popup.clone());
if let Some(popup_style) = styles.popup_style {
self.popup_style = popup_style;
}
if let Some(popup_scroll) = styles.popup_scroll {
self.popup_scroll = self.popup_scroll.map(|v| v.styles(popup_scroll));
}
self.popup_block = self.popup_block.map(|v| v.style(self.popup_style));
if let Some(border_style) = styles.popup_border {
self.popup_block = self.popup_block.map(|v| v.border_style(border_style));
}
if styles.popup_block.is_some() {
self.popup_block = styles.popup_block;
}
if styles.popup_len.is_some() {
self.popup_len = styles.popup_len;
}
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self.block = self.block.map(|v| v.style(self.style));
self
}
pub fn button_style(mut self, style: Style) -> Self {
self.button_style = Some(style);
self
}
pub fn select_style(mut self, style: Style) -> Self {
self.select_style = Some(style);
self
}
pub fn select_marker(mut self, marker: char) -> Self {
self.select_marker = Some(marker);
self
}
pub fn focus_style(mut self, style: Style) -> Self {
self.focus_style = Some(style);
self
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self.block = self.block.map(|v| v.style(self.style));
self
}
pub fn skip_item_render(mut self, skip: bool) -> Self {
self.skip_item_render = skip;
self
}
pub fn popup_alignment(mut self, alignment: Alignment) -> Self {
self.popup_alignment = alignment;
self
}
pub fn popup_placement(mut self, placement: Placement) -> Self {
self.popup_placement = placement;
self
}
pub fn popup_boundary(mut self, boundary: Rect) -> Self {
self.popup = self.popup.boundary(boundary);
self
}
pub fn popup_len(mut self, len: u16) -> Self {
self.popup_len = Some(len);
self
}
pub fn popup_style(mut self, style: Style) -> Self {
self.popup_style = style;
self
}
pub fn popup_block(mut self, block: Block<'a>) -> Self {
self.popup_block = Some(block);
self
}
pub fn popup_scroll(mut self, scroll: Scroll<'a>) -> Self {
self.popup_scroll = Some(scroll);
self
}
pub fn popup_offset(mut self, offset: (i16, i16)) -> Self {
self.popup = self.popup.offset(offset);
self
}
pub fn popup_x_offset(mut self, offset: i16) -> Self {
self.popup = self.popup.x_offset(offset);
self
}
pub fn popup_y_offset(mut self, offset: i16) -> Self {
self.popup = self.popup.y_offset(offset);
self
}
pub fn behave_focus(mut self, focus: ChoiceFocus) -> Self {
self.behave_focus = focus;
self
}
pub fn behave_select(mut self, select: ChoiceSelect) -> Self {
self.behave_select = select;
self
}
pub fn behave_close(mut self, close: ChoiceClose) -> Self {
self.behave_close = close;
self
}
pub fn width(&self) -> u16 {
let w = self
.items
.borrow()
.iter()
.map(|v| v.width())
.max()
.unwrap_or_default();
w as u16 + block_size(&self.block).width + 3
}
pub fn height(&self) -> u16 {
1 + block_size(&self.block).height
}
pub fn into_widgets(self) -> (ChoiceWidget<'a, T>, ChoicePopup<'a, T>) {
(
ChoiceWidget {
values: self.values,
default_value: self.default_value,
unknown_item: self.unknown_item,
items: self.items.clone(),
style: self.style,
button_style: self.button_style,
focus_style: self.focus_style,
block: self.block,
skip_item_render: self.skip_item_render,
len: self.popup_len,
behave_focus: self.behave_focus,
behave_select: self.behave_select,
behave_close: self.behave_close,
},
ChoicePopup {
items: self.items.clone(),
style: self.style,
select_style: self.select_style,
select_marker: self.select_marker,
popup: self.popup,
popup_style: self.popup_style,
popup_scroll: self.popup_scroll,
popup_block: self.popup_block,
popup_alignment: self.popup_alignment,
popup_placement: self.popup_placement,
popup_len: self.popup_len,
_phantom: Default::default(),
},
)
}
}
impl<'a, T> ChoiceWidget<'a, T>
where
T: PartialEq + Clone + Default,
{
pub fn width(&self) -> u16 {
let w = self
.items
.borrow()
.iter()
.map(|v| v.width())
.max()
.unwrap_or_default();
w as u16 + block_size(&self.block).width
}
pub fn height(&self) -> u16 {
1 + block_size(&self.block).height
}
}
impl<'a, T> StatefulWidget for &ChoiceWidget<'a, T>
where
T: PartialEq + Clone + Default,
{
type State = ChoiceState<T>;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
state.core.set_values(self.values.borrow().clone());
if let Some(default_value) = self.default_value.clone() {
state.core.set_default_value(Some(default_value));
}
render_choice(self, area, buf, state);
}
}
impl<T> StatefulWidget for ChoiceWidget<'_, T>
where
T: PartialEq + Clone + Default,
{
type State = ChoiceState<T>;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
state.core.set_values(self.values.take());
if let Some(default_value) = self.default_value.take() {
state.core.set_default_value(Some(default_value));
}
render_choice(&self, area, buf, state);
}
}
fn render_choice<T: PartialEq + Clone + Default>(
widget: &ChoiceWidget<'_, T>,
area: Rect,
buf: &mut Buffer,
state: &mut ChoiceState<T>,
) {
state.area = area;
state.behave_focus.set(widget.behave_focus);
state.behave_select = widget.behave_select;
state.behave_close = widget.behave_close;
if !state.popup.is_active() {
let len = widget
.len
.unwrap_or_else(|| min(5, widget.items.borrow().len()) as u16);
state.popup_scroll.max_offset = widget.items.borrow().len().saturating_sub(len as usize);
state.popup_scroll.page_len = len as usize;
if let Some(selected) = state.core.selected() {
state.popup_scroll.scroll_to_pos(selected);
}
}
state.nav_char.clear();
state.nav_char.extend(widget.items.borrow().iter().map(|v| {
v.spans
.first()
.and_then(|v| v.content.as_ref().chars().next())
.map_or(Vec::default(), |c| c.to_lowercase().collect::<Vec<_>>())
}));
state.inner = widget.block.inner_if_some(area);
state.item_area = Rect::new(
state.inner.x,
state.inner.y,
state.inner.width.saturating_sub(3),
state.inner.height,
);
state.button_area = Rect::new(
state
.inner
.right()
.saturating_sub(min(3, state.inner.width)),
state.inner.y,
3,
state.inner.height,
);
let style = widget.style;
let focus_style = widget.focus_style.unwrap_or(revert_style(widget.style));
if state.is_focused() {
if let Some(block) = &widget.block {
block.render(area, buf);
} else {
buf.set_style(state.inner, style);
}
buf.set_style(state.inner, focus_style);
} else {
if let Some(block) = &widget.block {
block.render(area, buf);
} else {
buf.set_style(state.inner, style);
}
if let Some(button_style) = widget.button_style {
buf.set_style(state.button_area, button_style);
}
}
if !widget.skip_item_render {
if let Some(selected) = state.core.selected() {
if let Some(item) = widget.items.borrow().get(selected) {
item.render(state.item_area, buf);
}
} else if let Some(unknown) = widget.unknown_item.as_ref() {
unknown.render(state.item_area, buf);
}
}
let dy = if (state.button_area.height & 1) == 1 {
state.button_area.height / 2
} else {
state.button_area.height.saturating_sub(1) / 2
};
let bc = if state.is_popup_active() {
" â—† "
} else {
" â–¼ "
};
Span::from(bc).render(
Rect::new(state.button_area.x, state.button_area.y + dy, 3, 1),
buf,
);
}
impl<T> ChoicePopup<'_, T>
where
T: PartialEq + Clone + Default,
{
pub fn layout(&self, area: Rect, buf: &mut Buffer, state: &mut ChoiceState<T>) -> Rect {
if state.popup.is_active() {
let len = min(
self.popup_len.unwrap_or(5),
self.items.borrow().len() as u16,
);
let padding = block_padding(&self.popup_block);
let popup_len = len + padding.top + padding.bottom;
let pop_area = Rect::new(0, 0, area.width, popup_len);
self.popup
.ref_constraint(
self.popup_placement
.into_constraint(self.popup_alignment, area),
)
.layout(pop_area, buf)
} else {
Rect::default()
}
}
}
impl<T> StatefulWidget for &ChoicePopup<'_, T>
where
T: PartialEq + Clone + Default,
{
type State = ChoiceState<T>;
fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_popup(self, buf, state);
}
}
impl<T> StatefulWidget for ChoicePopup<'_, T>
where
T: PartialEq + Clone + Default,
{
type State = ChoiceState<T>;
fn render(self, _area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_popup(&self, buf, state);
}
}
fn render_popup<T: PartialEq + Clone + Default>(
widget: &ChoicePopup<'_, T>,
buf: &mut Buffer,
state: &mut ChoiceState<T>,
) {
if state.popup.is_active() {
let popup_style = widget.popup_style;
let select_style = widget.select_style.unwrap_or(revert_style(widget.style));
{
let area = state.area;
let len = min(
widget.popup_len.unwrap_or(5),
widget.items.borrow().len() as u16,
);
let padding = block_padding(&widget.popup_block);
let popup_len = len + padding.top + padding.bottom;
let pop_area = Rect::new(0, 0, area.width, popup_len);
widget
.popup
.ref_constraint(
widget
.popup_placement
.into_constraint(widget.popup_alignment, area),
)
.render(pop_area, buf, &mut state.popup);
}
let sa = ScrollArea::new()
.style(fallback_popup_style(widget.popup_style))
.block(widget.popup_block.as_ref())
.v_scroll(widget.popup_scroll.as_ref());
let inner = sa.inner(state.popup.area, None, Some(&state.popup_scroll));
sa.render(
state.popup.area,
buf,
&mut ScrollAreaState::new().v_scroll(&mut state.popup_scroll),
);
state.popup_scroll.max_offset = widget
.items
.borrow()
.len()
.saturating_sub(inner.height as usize);
state.popup_scroll.page_len = inner.height as usize;
state.item_areas.clear();
let mut row = inner.y;
let mut idx = state.popup_scroll.offset;
let hidden_marker;
let (marker_len, marker, no_marker) = {
if let Some(marker) = widget.select_marker {
hidden_marker = marker.to_string();
if unicode_display_width::is_double_width(marker) {
(2, Some(hidden_marker.as_str()), Some(" "))
} else {
(1, Some(hidden_marker.as_str()), Some(" "))
}
} else {
(0, None, None)
}
};
loop {
if row >= inner.bottom() {
break;
}
let item_area = Rect::new(inner.x, row, inner.width, 1);
state.item_areas.push(item_area);
let (style, marker) = if state.core.selected() == Some(idx) {
(popup_style.patch(select_style), marker)
} else {
(popup_style, no_marker)
};
buf.set_style(item_area, style);
if let Some(item) = widget.items.borrow().get(idx) {
if let Some(marker) = &marker {
marker.render(
Rect::new(item_area.x, item_area.y, marker_len, item_area.height),
buf,
);
item.render(
Rect::new(
item_area.x + marker_len,
item_area.y,
item_area.width.saturating_sub(marker_len),
item_area.height,
),
buf,
);
} else {
item.render(item_area, buf);
}
} else {
}
row += 1;
idx += 1;
}
} else {
state.popup.clear_areas();
}
}
impl<T> Clone for ChoiceState<T>
where
T: PartialEq + Clone + Default,
{
fn clone(&self) -> Self {
let popup = self.popup.clone();
let behave_focus = Rc::new(Cell::new(self.behave_focus.get()));
let focus = focus_cb(
popup.active.clone(),
behave_focus.clone(),
self.focus.new_instance(),
);
Self {
area: self.area,
inner: self.inner,
nav_char: self.nav_char.clone(),
item_area: self.item_area,
button_area: self.button_area,
item_areas: self.item_areas.clone(),
core: self.core.clone(),
popup,
popup_scroll: self.popup_scroll.clone(),
behave_focus,
behave_select: self.behave_select,
behave_close: self.behave_close,
focus,
mouse: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl<T> Default for ChoiceState<T>
where
T: PartialEq + Clone + Default,
{
fn default() -> Self {
let popup = PopupCoreState::default();
let behave_focus = Rc::new(Cell::new(ChoiceFocus::default()));
let focus = focus_cb(
popup.active.clone(),
behave_focus.clone(),
Default::default(),
);
Self {
area: Default::default(),
inner: Default::default(),
nav_char: Default::default(),
item_area: Default::default(),
button_area: Default::default(),
item_areas: Default::default(),
core: Default::default(),
popup,
popup_scroll: Default::default(),
behave_focus,
behave_select: Default::default(),
behave_close: Default::default(),
focus,
mouse: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
fn focus_cb(
active: Rc<Cell<bool>>,
behave_focus: Rc<Cell<ChoiceFocus>>,
flag: FocusFlag,
) -> FocusFlag {
let active_clone = active.clone();
flag.on_lost(move || {
if active_clone.get() {
active_clone.set(false);
}
});
flag.on_gained(move || {
if !active.get() {
if behave_focus.get() == ChoiceFocus::OpenOnFocusGained {
active.set(true);
}
}
});
flag
}
impl<T> HasFocus for ChoiceState<T>
where
T: PartialEq + Clone + Default,
{
fn build(&self, builder: &mut FocusBuilder) {
builder.widget_with_flags(self.focus(), self.area(), 0, self.navigable());
builder.widget_with_flags(self.focus(), self.popup.area, 1, Navigation::Mouse);
}
fn focus(&self) -> FocusFlag {
self.focus.clone()
}
fn area(&self) -> Rect {
self.area
}
}
impl<T> HasScreenCursor for ChoiceState<T>
where
T: PartialEq + Clone + Default,
{
fn screen_cursor(&self) -> Option<(u16, u16)> {
None
}
}
impl<T> RelocatableState for ChoiceState<T>
where
T: PartialEq + Clone + Default,
{
fn relocate(&mut self, _shift: (i16, i16), _clip: Rect) {
}
fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
self.area.relocate(shift, clip);
self.inner.relocate(shift, clip);
self.item_area.relocate(shift, clip);
self.button_area.relocate(shift, clip);
self.item_areas.relocate(shift, clip);
self.popup.relocate_popup(shift, clip);
self.popup_scroll.relocate(shift, clip);
}
}
impl<T> ChoiceState<T>
where
T: PartialEq + Clone + Default,
{
pub fn new() -> Self {
Self::default()
}
pub fn named(name: &str) -> Self {
let mut z = Self::default();
z.focus = z.focus.with_name(name);
z
}
pub fn is_popup_active(&self) -> bool {
self.popup.is_active()
}
pub fn flip_popup_active(&mut self) {
self.popup.flip_active();
}
pub fn set_popup_active(&mut self, active: bool) -> bool {
self.popup.set_active(active)
}
pub fn set_default_value(&mut self, default_value: Option<T>) {
self.core.set_default_value(default_value);
}
pub fn default_value(&self) -> &Option<T> {
self.core.default_value()
}
pub fn set_value(&mut self, value: T) -> bool {
self.core.set_value(value)
}
pub fn value(&self) -> T {
self.core.value()
}
pub fn clear(&mut self) -> bool {
self.core.clear()
}
pub fn select(&mut self, select: usize) -> bool {
self.core.set_selected(select)
}
pub fn selected(&self) -> Option<usize> {
self.core.selected()
}
pub fn is_empty(&self) -> bool {
self.core.values().is_empty()
}
pub fn len(&self) -> usize {
self.core.values().len()
}
pub fn clear_offset(&mut self) {
self.popup_scroll.set_offset(0);
}
pub fn set_offset(&mut self, offset: usize) -> bool {
self.popup_scroll.set_offset(offset)
}
pub fn offset(&self) -> usize {
self.popup_scroll.offset()
}
pub fn max_offset(&self) -> usize {
self.popup_scroll.max_offset()
}
pub fn page_len(&self) -> usize {
self.popup_scroll.page_len()
}
pub fn scroll_by(&self) -> usize {
self.popup_scroll.scroll_by()
}
pub fn scroll_to_selected(&mut self) -> bool {
if let Some(selected) = self.core.selected() {
self.popup_scroll.scroll_to_pos(selected)
} else {
false
}
}
}
impl<T> ChoiceState<T>
where
T: PartialEq + Clone + Default,
{
pub fn select_by_char(&mut self, c: char) -> bool {
if self.nav_char.is_empty() {
return false;
}
let c = c.to_lowercase().collect::<Vec<_>>();
let (mut idx, end_loop) = if let Some(idx) = self.core.selected() {
(idx + 1, idx)
} else {
if self.nav_char[0] == c {
self.core.set_selected(0);
return true;
} else {
(1, 0)
}
};
loop {
if idx >= self.nav_char.len() {
idx = 0;
}
if idx == end_loop {
break;
}
if self.nav_char[idx] == c {
self.core.set_selected(idx);
return true;
}
idx += 1;
}
false
}
pub fn reverse_select_by_char(&mut self, c: char) -> bool {
if self.nav_char.is_empty() {
return false;
}
let c = c.to_lowercase().collect::<Vec<_>>();
let (mut idx, end_loop) = if let Some(idx) = self.core.selected() {
if idx == 0 {
(self.nav_char.len() - 1, 0)
} else {
(idx - 1, idx)
}
} else {
if self.nav_char.last() == Some(&c) {
self.core.set_selected(self.nav_char.len() - 1);
return true;
} else {
(self.nav_char.len() - 1, 0)
}
};
loop {
if self.nav_char[idx] == c {
self.core.set_selected(idx);
return true;
}
if idx == end_loop {
break;
}
if idx == 0 {
idx = self.nav_char.len() - 1;
} else {
idx -= 1;
}
}
false
}
pub fn move_to(&mut self, n: usize) -> ChoiceOutcome {
let old_selected = self.selected();
let r1 = self.popup.set_active(true);
let r2 = self.select(n);
let r3 = self.scroll_to_selected();
if old_selected != self.selected() {
ChoiceOutcome::Value
} else if r1 || r2 || r3 {
ChoiceOutcome::Changed
} else {
ChoiceOutcome::Continue
}
}
pub fn move_down(&mut self, n: usize) -> ChoiceOutcome {
if self.core.is_empty() {
return ChoiceOutcome::Continue;
}
let old_selected = self.selected();
let r1 = self.popup.set_active(true);
let idx = if let Some(idx) = self.core.selected() {
idx + n
} else {
n.saturating_sub(1)
};
let idx = idx.clamp(0, self.len() - 1);
let r2 = self.core.set_selected(idx);
let r3 = self.scroll_to_selected();
if old_selected != self.selected() {
ChoiceOutcome::Value
} else if r1 || r2 || r3 {
ChoiceOutcome::Changed
} else {
ChoiceOutcome::Continue
}
}
pub fn move_up(&mut self, n: usize) -> ChoiceOutcome {
if self.core.is_empty() {
return ChoiceOutcome::Continue;
}
let old_selected = self.selected();
let r1 = self.popup.set_active(true);
let idx = if let Some(idx) = self.core.selected() {
idx.saturating_sub(n)
} else {
0
};
let idx = idx.clamp(0, self.len() - 1);
let r2 = self.core.set_selected(idx);
let r3 = self.scroll_to_selected();
if old_selected != self.selected() {
ChoiceOutcome::Value
} else if r1 || r2 || r3 {
ChoiceOutcome::Changed
} else {
ChoiceOutcome::Continue
}
}
}
impl<T: PartialEq + Clone + Default> HandleEvent<Event, Popup, ChoiceOutcome> for ChoiceState<T> {
fn handle(&mut self, event: &Event, _qualifier: Popup) -> ChoiceOutcome {
let r = if self.is_focused() {
match event {
ct_event!(key press ' ') | ct_event!(keycode press Enter) => {
self.flip_popup_active();
ChoiceOutcome::Changed
}
ct_event!(keycode press Esc) => {
if self.set_popup_active(false) {
ChoiceOutcome::Changed
} else {
ChoiceOutcome::Continue
}
}
ct_event!(key press c) => {
if self.select_by_char(*c) {
self.scroll_to_selected();
ChoiceOutcome::Value
} else {
ChoiceOutcome::Continue
}
}
ct_event!(key press SHIFT-c) => {
if self.reverse_select_by_char(*c) {
self.scroll_to_selected();
ChoiceOutcome::Value
} else {
ChoiceOutcome::Continue
}
}
ct_event!(keycode press Delete) | ct_event!(keycode press Backspace) => {
if self.clear() {
ChoiceOutcome::Value
} else {
ChoiceOutcome::Continue
}
}
ct_event!(keycode press Down) => self.move_down(1),
ct_event!(keycode press Up) => self.move_up(1),
ct_event!(keycode press PageUp) => self.move_up(self.page_len()),
ct_event!(keycode press PageDown) => self.move_down(self.page_len()),
ct_event!(keycode press Home) => self.move_to(0),
ct_event!(keycode press End) => self.move_to(self.len().saturating_sub(1)),
_ => ChoiceOutcome::Continue,
}
} else {
ChoiceOutcome::Continue
};
if !r.is_consumed() {
self.handle(event, MouseOnly)
} else {
r
}
}
}
impl<T: PartialEq + Clone + Default> HandleEvent<Event, MouseOnly, ChoiceOutcome>
for ChoiceState<T>
{
fn handle(&mut self, event: &Event, _qualifier: MouseOnly) -> ChoiceOutcome {
let r0 = handle_mouse(self, event);
let r1 = handle_select(self, event);
let r2 = handle_close(self, event);
let mut r = max(r0, max(r1, r2));
r = r.or_else(|| mouse_trap(event, self.popup.area).into());
r
}
}
fn handle_mouse<T: PartialEq + Clone + Default>(
state: &mut ChoiceState<T>,
event: &Event,
) -> ChoiceOutcome {
match event {
ct_event!(mouse down Left for x,y)
if state.item_area.contains((*x, *y).into())
|| state.button_area.contains((*x, *y).into()) =>
{
if !state.gained_focus() {
state.flip_popup_active();
ChoiceOutcome::Changed
} else {
ChoiceOutcome::Continue
}
}
ct_event!(mouse down Left for x,y)
| ct_event!(mouse down Right for x,y)
| ct_event!(mouse down Middle for x,y)
if !state.item_area.contains((*x, *y).into())
&& !state.button_area.contains((*x, *y).into()) =>
{
match state.popup.handle(event, Popup) {
PopupOutcome::Hide => {
state.set_popup_active(false);
ChoiceOutcome::Changed
}
r => r.into(),
}
}
_ => ChoiceOutcome::Continue,
}
}
fn handle_select<T: PartialEq + Clone + Default>(
state: &mut ChoiceState<T>,
event: &Event,
) -> ChoiceOutcome {
match state.behave_select {
ChoiceSelect::MouseScroll => {
let mut sas = ScrollAreaState::new()
.area(state.popup.area)
.v_scroll(&mut state.popup_scroll);
let mut r = match sas.handle(event, MouseOnly) {
ScrollOutcome::Up(n) => state.move_up(n),
ScrollOutcome::Down(n) => state.move_down(n),
ScrollOutcome::VPos(n) => state.move_to(n),
_ => ChoiceOutcome::Continue,
};
r = r.or_else(|| match event {
ct_event!(mouse down Left for x,y)
if state.popup.area.contains((*x, *y).into()) =>
{
if let Some(n) = item_at(&state.item_areas, *x, *y) {
state.move_to(state.offset() + n)
} else {
ChoiceOutcome::Unchanged
}
}
ct_event!(mouse drag Left for x,y)
if state.popup.area.contains((*x, *y).into()) =>
{
if let Some(n) = item_at(&state.item_areas, *x, *y) {
state.move_to(state.offset() + n)
} else {
ChoiceOutcome::Unchanged
}
}
_ => ChoiceOutcome::Continue,
});
r
}
ChoiceSelect::MouseMove => {
let mut r = if let Some(selected) = state.core.selected() {
let rel_sel = selected.saturating_sub(state.offset());
let mut sas = ScrollAreaState::new()
.area(state.popup.area)
.v_scroll(&mut state.popup_scroll);
match sas.handle(event, MouseOnly) {
ScrollOutcome::Up(n) => {
state.popup_scroll.scroll_up(n);
if state.select(state.offset() + rel_sel) {
ChoiceOutcome::Value
} else {
ChoiceOutcome::Unchanged
}
}
ScrollOutcome::Down(n) => {
state.popup_scroll.scroll_down(n);
if state.select(state.offset() + rel_sel) {
ChoiceOutcome::Value
} else {
ChoiceOutcome::Unchanged
}
}
ScrollOutcome::VPos(n) => {
if state.popup_scroll.set_offset(n) {
ChoiceOutcome::Value
} else {
ChoiceOutcome::Unchanged
}
}
_ => ChoiceOutcome::Continue,
}
} else {
ChoiceOutcome::Continue
};
r = r.or_else(|| match event {
ct_event!(mouse moved for x,y) if state.popup.area.contains((*x, *y).into()) => {
if let Some(n) = item_at(&state.item_areas, *x, *y) {
state.move_to(state.offset() + n)
} else {
ChoiceOutcome::Unchanged
}
}
_ => ChoiceOutcome::Continue,
});
r
}
ChoiceSelect::MouseClick => {
let mut sas = ScrollAreaState::new()
.area(state.popup.area)
.v_scroll(&mut state.popup_scroll);
let mut r = match sas.handle(event, MouseOnly) {
ScrollOutcome::Up(n) => {
if state.popup_scroll.scroll_up(n) {
ChoiceOutcome::Changed
} else {
ChoiceOutcome::Unchanged
}
}
ScrollOutcome::Down(n) => {
if state.popup_scroll.scroll_down(n) {
ChoiceOutcome::Changed
} else {
ChoiceOutcome::Unchanged
}
}
ScrollOutcome::VPos(n) => {
if state.popup_scroll.set_offset(n) {
ChoiceOutcome::Changed
} else {
ChoiceOutcome::Unchanged
}
}
_ => ChoiceOutcome::Continue,
};
r = r.or_else(|| match event {
ct_event!(mouse down Left for x,y)
if state.popup.area.contains((*x, *y).into()) =>
{
if let Some(n) = item_at(&state.item_areas, *x, *y) {
state.move_to(state.offset() + n)
} else {
ChoiceOutcome::Unchanged
}
}
ct_event!(mouse drag Left for x,y)
if state.popup.area.contains((*x, *y).into()) =>
{
if let Some(n) = item_at(&state.item_areas, *x, *y) {
state.move_to(state.offset() + n)
} else {
ChoiceOutcome::Unchanged
}
}
_ => ChoiceOutcome::Continue,
});
r
}
}
}
fn handle_close<T: PartialEq + Clone + Default>(
state: &mut ChoiceState<T>,
event: &Event,
) -> ChoiceOutcome {
match state.behave_close {
ChoiceClose::SingleClick => match event {
ct_event!(mouse down Left for x,y) if state.popup.area.contains((*x, *y).into()) => {
if let Some(n) = item_at(&state.item_areas, *x, *y) {
let r = state.move_to(state.offset() + n);
let s = if state.set_popup_active(false) {
ChoiceOutcome::Changed
} else {
ChoiceOutcome::Unchanged
};
max(r, s)
} else {
ChoiceOutcome::Unchanged
}
}
_ => ChoiceOutcome::Continue,
},
ChoiceClose::DoubleClick => match event {
ct_event!(mouse any for m) if state.mouse.doubleclick(state.popup.area, m) => {
if let Some(n) = item_at(&state.item_areas, m.column, m.row) {
let r = state.move_to(state.offset() + n);
let s = if state.set_popup_active(false) {
ChoiceOutcome::Changed
} else {
ChoiceOutcome::Unchanged
};
max(r, s)
} else {
ChoiceOutcome::Unchanged
}
}
_ => ChoiceOutcome::Continue,
},
}
}
pub fn handle_events<T: PartialEq + Clone + Default>(
state: &mut ChoiceState<T>,
focus: bool,
event: &Event,
) -> ChoiceOutcome {
state.focus.set(focus);
HandleEvent::handle(state, event, Popup)
}
pub fn handle_mouse_events<T: PartialEq + Clone + Default>(
state: &mut ChoiceState<T>,
event: &Event,
) -> ChoiceOutcome {
HandleEvent::handle(state, event, MouseOnly)
}