use crate::component::{Component, EventCx, MeasureCx};
use crate::event::Event;
use crate::geom::{Rect, Size};
use crate::layout::Constraint;
use crate::render::RenderCx;
use crate::style::Style;
use crate::text::Text;
pub struct Select {
options: Vec<Text>,
selected: usize,
expanded: bool,
focused: bool,
rect: Rect,
style: Style,
selected_style: Style,
}
impl Select {
pub fn new() -> Self {
Self {
options: Vec::new(),
selected: 0,
expanded: false,
focused: false,
rect: Rect::default(),
style: Style::default(),
selected_style: Style::default().bg(crate::style::Color::White).fg(crate::style::Color::Black),
}
}
pub fn options(mut self, options: Vec<impl Into<Text>>) -> Self {
self.options = options.into_iter().map(|o| o.into()).collect();
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn selected_style(mut self, style: Style) -> Self {
self.selected_style = style;
self
}
pub fn selected(&self) -> usize {
self.selected
}
pub fn selected_text(&self) -> &str {
self.options.get(self.selected).map(|t| t.first_text()).unwrap_or("")
}
pub fn set_selected(&mut self, index: usize, cx: &mut EventCx) {
if index < self.options.len() {
self.selected = index;
cx.invalidate_paint();
}
}
}
impl Component for Select {
fn render(&self, cx: &mut RenderCx) {
if self.options.is_empty() {
return;
}
let indicator = if self.expanded { "▲" } else { "▼" };
let display = format!("{} {}", self.selected_text(), indicator);
if self.focused {
cx.set_style(self.selected_style.clone());
} else {
cx.set_style(self.style.clone());
}
cx.line(&display);
if self.expanded {
for (i, opt) in self.options.iter().enumerate() {
if i == self.selected {
cx.set_style(self.selected_style.clone());
cx.text("❯ ");
} else {
cx.set_style(self.style.clone());
cx.text(" ");
}
cx.line(opt.first_text());
}
}
}
fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
if self.options.is_empty() {
return Size { width: 0, height: 0 };
}
let max_w = self.options.iter().map(|o| o.max_width()).max().unwrap_or(0) + 2;
let height = if self.expanded {
1u16.saturating_add(self.options.len() as u16)
} else {
1
};
Size { width: max_w, height }
}
fn event(&mut self, event: &Event, cx: &mut EventCx) {
match event {
Event::Focus => {
self.focused = true;
cx.invalidate_paint();
return;
}
Event::Blur => {
self.focused = false;
self.expanded = false;
cx.invalidate_layout();
return;
}
_ => {}
}
if self.options.is_empty() { return; }
if cx.phase() != crate::event::EventPhase::Target { return; }
if let Event::Key(key_event) = event {
match &key_event.key {
crate::event::Key::Enter | crate::event::Key::Char(' ') => {
if self.expanded {
self.expanded = false;
} else {
self.expanded = true;
}
cx.invalidate_layout();
return;
}
crate::event::Key::Esc => {
if self.expanded {
self.expanded = false;
cx.invalidate_layout();
}
return;
}
crate::event::Key::Up => {
if self.expanded {
if self.selected > 0 {
self.selected -= 1;
} else {
self.selected = self.options.len() - 1;
}
cx.invalidate_paint();
}
return;
}
crate::event::Key::Down => {
if self.expanded {
if self.selected + 1 < self.options.len() {
self.selected += 1;
} else {
self.selected = 0;
}
cx.invalidate_paint();
}
return;
}
_ => {}
}
}
}
fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
self.rect = rect;
}
fn focusable(&self) -> bool {
true
}
fn style(&self) -> Style {
self.style.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testbuffer::TestBuffer;
#[test]
fn test_collapsed() {
let mut tb = TestBuffer::new(20, 1);
tb.render(&Select::new().options(vec![Text::from("A")]));
assert!(tb.buffer.cells[0].symbol.contains("A"));
}
}