use std::sync::atomic::{AtomicBool, Ordering};
use super::*;
use crate::canvas::*;
use std::num::NonZero;
use std::rc::Rc;
use as_any::Downcast;
use crossterm::event::{Event, KeyCode};
use unicode_width::UnicodeWidthStr;
pub type ComboboxSelectionChangedCallback<T> = dyn Fn(&mut Combobox<T>);
pub type ComboboxKeyHandler<T> = fn(&mut Combobox<T>, event: &mut WindowEvent);
pub struct Combobox<T: PartialEq + 'static> {
has_focus: bool,
border_visible: AtomicBool,
border_bg: Option<Color>,
border_fg: Option<Color>,
base: WidgetBase,
items: Vec<DisplayValue<T>>,
focus: usize,
callback: Rc<ComboboxSelectionChangedCallback<T>>,
key_handler: ComboboxKeyHandler<T>,
}
impl<T: PartialEq> Default for Combobox<T> {
fn default() -> Self {
let mut s = Self {
items: Vec::new(),
has_focus: false,
border_visible: AtomicBool::default(),
border_fg: None,
border_bg: None,
base: WidgetBase::default(),
focus: usize::MIN,
callback: Rc::new(Self::default_callback),
key_handler: Self::default_key_handler,
};
s.base.border = BorderStyle::Custom(Self::render_border, Thickness::new(1, 3, 3, 1));
s.base.constraints.x.min = NonZero::new(3).unwrap();
s.base.constraints.y.min = NonZero::<TSize>::MIN;
s.base.constraints.y.max = Size::Fixed(NonZero::<TSize>::MIN);
s
}
}
impl<T: PartialEq> Combobox<T> {
pub const INDICATOR_LEFT: Grapheme = ScrollViewer::ARROW_LEFT;
pub const INDICATOR_RIGHT: Grapheme = ScrollViewer::ARROW_RIGHT;
pub const DEFAULT_BORDER: BorderKind = BorderKind::Solid;
pub fn default_callback(_: &mut Self) {}
pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
if let Event::Key(k) = event.raw() {
if self.base.enabled {
if k.code == KeyCode::Left {
self.select_previous();
event.handled = true;
}
else if k.code == KeyCode::Right {
self.select_next();
event.handled = true;
}
}
}
}
pub fn set_key_handler(&mut self, f: ComboboxKeyHandler<T>) {
self.key_handler = f;
}
pub fn set_selection_changed_callback<F: Fn(&mut Self) + 'static>(&mut self, callback: F) {
self.callback = Rc::new(callback);
}
pub fn get_value(&self) -> Option<&DisplayValue<T>> {
self.items.get(self.focus)
}
pub fn set_border_fg(&mut self, color: Option<Color>) {
self.border_fg = color;
}
pub fn set_border_bg(&mut self, color: Option<Color>) {
self.border_bg = color;
}
pub fn select_next(&mut self) {
if self.focus < self.items.len() - 1 {
let cb = self.callback.clone();
self.focus += 1;
cb(self);
}
}
pub fn select_previous(&mut self) {
if self.focus > 0 {
let cb = self.callback.clone();
self.focus -= 1;
cb(self);
}
}
fn render_border(w: &dyn Window, mut canvas: BorderCanvas<'_>) {
let slf: &Self = w.downcast_ref().unwrap();
slf.border_visible.store(true, Ordering::Release);
let corners = Self::DEFAULT_BORDER.corner_style();
let lines = Self::DEFAULT_BORDER.line_style();
let conns = Self::DEFAULT_BORDER.connector_style();
let connector_bottom_lr = conns[2];
let connector_top_lr = conns[3];
{
let w = canvas.size().x;
canvas
.get_glyph(0, 1)
.unwrap()
.set_grapheme(lines[1])
.unwrap();
canvas
.get_glyph(1, 1)
.unwrap()
.set_grapheme(Self::INDICATOR_LEFT)
.unwrap();
canvas
.get_glyph(2, 1)
.unwrap()
.set_grapheme(lines[1])
.unwrap();
canvas
.get_glyph(w - 3, 1)
.unwrap()
.set_grapheme(lines[1])
.unwrap();
canvas
.get_glyph(w - 2, 1)
.unwrap()
.set_grapheme(Self::INDICATOR_RIGHT)
.unwrap();
canvas
.get_glyph(w - 1, 1)
.unwrap()
.set_grapheme(lines[1])
.unwrap();
}
let mut row = canvas.get_row(0).unwrap();
row.get(0).unwrap().set_grapheme(corners[0]).unwrap();
row.get(1).unwrap().set_grapheme(lines[0]).unwrap();
row.get(2)
.unwrap()
.set_grapheme(connector_bottom_lr)
.unwrap();
row.get(row.length() - 1)
.unwrap()
.set_grapheme(corners[1])
.unwrap();
row.get(row.length() - 2)
.unwrap()
.set_grapheme(lines[0])
.unwrap();
row.get(row.length() - 3)
.unwrap()
.set_grapheme(connector_bottom_lr)
.unwrap();
for idx in 3..row.length() - 3 {
row.get(idx).unwrap().set_grapheme(lines[0]).unwrap();
}
let mut row = canvas.get_row(canvas.size().y - 1).unwrap();
row.get(0).unwrap().set_grapheme(corners[2]).unwrap();
row.get(1).unwrap().set_grapheme(lines[0]).unwrap();
row.get(2).unwrap().set_grapheme(connector_top_lr).unwrap();
row.get(row.length() - 1)
.unwrap()
.set_grapheme(corners[3])
.unwrap();
row.get(row.length() - 2)
.unwrap()
.set_grapheme(lines[0])
.unwrap();
row.get(row.length() - 3)
.unwrap()
.set_grapheme(connector_top_lr)
.unwrap();
for idx in 3..row.length() - 3 {
row.get(idx).unwrap().set_grapheme(lines[0]).unwrap();
}
if let Some(color) = slf.border_fg {
canvas.fill_foreground(color);
}
if let Some(color) = slf.border_bg {
canvas.fill_background(color);
}
}
}
impl<T: PartialEq> ItemCollection<DisplayValue<T>> for Combobox<T> {
type Index = usize;
fn add_item(&mut self, item: DisplayValue<T>) {
self.items.push(item);
self.items
.sort_by(|a, b| a.display.as_str().cmp(b.display.as_str()));
self.provoke_changed_property(WindowProperty::Children);
}
fn remove_item(&mut self, item: &DisplayValue<T>) {
if let Some(idx) = self.items.iter().position(|x| x.value == item.value) {
self.items.remove(idx);
if idx == self.focus {
let cb = self.callback.clone();
cb(self);
}
}
}
fn remove_at(&mut self, index: Self::Index) -> DisplayValue<T> {
self.items.remove(index)
}
fn get_item(&mut self, index: Self::Index) -> Option<&DisplayValue<T>> {
self.items.get(index)
}
fn clear_items(&mut self) {
if !self.items.is_empty() {
self.items.clear();
self.focus = usize::MIN;
self.provoke_changed_property(WindowProperty::Focus);
let cb = self.callback.clone();
cb(self);
}
}
fn items(&self) -> Self::Index {
self.items.len()
}
}
impl<T: PartialEq> Window for Combobox<T> {
fn render(&self, canvas: &mut Canvas) {
let has_border = self.border_visible.load(Ordering::Relaxed);
if !has_border {
let mut row = canvas.get_row(0, GlyphWidth::Half).unwrap();
row.get(0)
.unwrap()
.set_grapheme(Self::INDICATOR_LEFT)
.unwrap();
row.get(row.length() - 1)
.unwrap()
.set_grapheme(Self::INDICATOR_RIGHT)
.unwrap();
}
let mut row = canvas.get_row_variable_width(0).unwrap();
if let Some(v) = self.items.get(self.focus) {
let cb_index = format!("[{}/{}]", self.focus + 1, self.items.len());
let index_w = cb_index.width() as TSize;
let w = v.display.width() as TSize;
let width = row.width();
let style = Style::Reverse.when(self.has_focus);
row = row.with_custom_width(width).unwrap();
if row.width() > w + index_w {
row.skip((row.width() - w - (index_w + 1)) / 2);
}
row.add_string(
&v.display,
None,
None,
style,
Some(VariableWidthGlyphRow::DOT3_REPLACEMENT),
)
.unwrap();
row.skip(1);
row.add_string(
&cb_index,
self.base.colors.base_fg,
self.base.colors.base_bg,
Style::default(),
None,
)
.ok();
}
}
fn handle_event(&mut self, event: &mut WindowEvent) {
match event.raw() {
Event::Resize(_, _) => self.border_visible.store(false, Ordering::Relaxed),
Event::FocusGained => self.has_focus = true,
Event::FocusLost => self.has_focus = false,
_ => (self.key_handler)(self, event),
}
}
fn is_enabled(&self) -> bool {
self.base.enabled
}
}
impl<T: PartialEq> WindowLayout for Combobox<T> {
fn desired_size(&self, available_size: TPoint) -> TPoint {
self.base.desired_size(available_size)
}
fn border(&self) -> BorderStyle {
self.base.border
}
fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
self.base.alignment
}
fn is_visible(&self) -> bool {
self.base.visibility
}
fn margin(&self) -> Thickness {
self.base.margin
}
}
impl<T: PartialEq + 'static> HasWindowUID for Combobox<T> {
fn uid(&self) -> WindowUID {
self.base.uid
}
}
impl<T: PartialEq> Widget for Combobox<T> {
fn set_visibility(&mut self, visibility: bool) {
self.base.visibility = visibility;
self.provoke_changed_property(WindowProperty::IsVisible);
}
fn set_width(&mut self, width: Size) {
self.base.size.x = width;
self.provoke_changed_property(WindowProperty::Size);
}
fn set_height(&mut self, height: Size) {
let _ = height;
}
fn set_margin(&mut self, margin: Thickness) {
self.base.margin = margin;
self.provoke_changed_property(WindowProperty::Margin);
}
fn set_border(&mut self, border: BorderStyle) {
let _ = border;
}
fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
self.base.alignment = (horizontal, vertical);
self.provoke_changed_property(WindowProperty::Alignment);
}
fn set_enabled_state(&mut self, is_enabled: bool) {
self.base.enabled = is_enabled;
}
fn set_width_constraint(&mut self, width: SizeConstraint) {
self.base.constraints.x = width;
self.provoke_changed_property(WindowProperty::Size);
}
fn set_height_constraint(&mut self, height: SizeConstraint) {
self.base.constraints.y = height;
self.provoke_changed_property(WindowProperty::Size);
}
}
impl<T: PartialEq> WidgetColors for Combobox<T> {
fn set_disabled_color(&mut self, color: Option<Color>) {
self.base.colors.disabled = color;
}
fn set_base_fg_color(&mut self, color: Option<Color>) {
self.base.colors.base_fg = color;
}
fn set_base_bg_color(&mut self, color: Option<Color>) {
self.base.colors.base_bg = color;
}
}