use std::marker::PhantomData;
use crate::prelude::*;
const MINSPACE_FOR_DRAWING: u32 = 5;
const MIN_WIDTH_VARIANT_NAME: u32 = 6;
const MINSPACE_FOR_DROPBUTTON_DRAWING: u32 = 3;
#[repr(u8)]
#[derive(Copy, Clone, PartialEq, Eq)]
enum ButtonState {
Hidden,
Normal,
Hovered,
Inactive,
Pressed,
}
impl ButtonState {
#[inline(always)]
fn color(&self, theme: &Theme) -> CharAttribute {
match self {
ButtonState::Hidden => theme.menu.text.inactive,
ButtonState::Normal => theme.menu.text.normal,
ButtonState::Hovered => theme.menu.text.hovered,
ButtonState::Inactive => theme.menu.text.inactive,
ButtonState::Pressed => theme.menu.text.pressed_or_selected,
}
}
#[inline(always)]
fn update_state(&mut self, check: bool, expected_value: ButtonState) -> bool {
if (check) && (*self != expected_value) {
*self = expected_value;
return true;
}
if (!check) && (*self == expected_value) {
*self = ButtonState::Normal;
return true;
}
false
}
#[inline(always)]
fn is_accesible(&self) -> bool {
!matches!(self, ButtonState::Hidden | ButtonState::Inactive)
}
}
pub(crate) trait ComboBoxComponentDataProvider {
fn count(&self) -> u32;
fn name(&self, index: u32) -> Option<&str>;
fn description(&self, index: u32) -> Option<&str>;
fn symbol(&self, index: u32) -> Option<&str>;
}
pub(crate) struct ComboBoxComponent<DataProvider>
where
DataProvider: ComboBoxComponentDataProvider,
{
pub(crate) current_index: u32,
start_index: u32,
mouse_index: u32,
header_y_ofs: i32,
expanded_panel_y: i32,
allow_none_value: bool,
show_description: bool,
expanded_size: Size,
count: u32,
symbol_size: u8,
button_up: ButtonState,
button_down: ButtonState,
_phantom: PhantomData<DataProvider>,
none_repr: String,
}
impl<T> ComboBoxComponent<T>
where
T: ComboBoxComponentDataProvider,
{
pub(crate) fn new(allow_none_value: bool, show_description: bool, count: u32, symbol_size: u8) -> Self {
Self {
start_index: 0,
current_index: 0,
header_y_ofs: 0,
expanded_panel_y: 1,
mouse_index: u32::MAX,
allow_none_value,
show_description,
expanded_size: Size::default(),
count,
symbol_size,
button_up: ButtonState::Hidden,
button_down: ButtonState::Hidden,
none_repr: String::new(),
_phantom: PhantomData,
}
}
fn visible_items(&self) -> u32 {
let height = self.expanded_size.height;
if height > 3 {
height - 3
} else {
1
}
}
pub(crate) fn update_current_index(&mut self, pos: u32) {
let expanded_size = self.expanded_size;
let visible_items = if expanded_size.height > 3 { expanded_size.height - 3 } else { 1 };
let count = self.count;
if count > 0 {
let last_item_index = if self.allow_none_value { count } else { count - 1 };
self.current_index = pos.min(last_item_index);
if self.start_index >= self.current_index {
self.start_index = self.current_index;
} else if self.start_index + visible_items <= self.current_index {
self.start_index = self.current_index + 1 - visible_items;
}
if self.start_index + visible_items > (last_item_index + 1) {
self.start_index = last_item_index + 1 - visible_items;
}
}
self.update_button_states();
}
pub(crate) fn clear(&mut self) {
self.count = 0;
self.start_index = 0;
self.current_index = 0;
self.button_up = ButtonState::Hidden;
self.button_down = ButtonState::Hidden;
self.mouse_index = u32::MAX;
}
fn update_button_states(&mut self) {
if self.button_up == ButtonState::Hidden {
return;
}
self.button_up = if self.current_index == 0 {
ButtonState::Inactive
} else {
ButtonState::Normal
};
if self.count > 0 {
let last = if self.allow_none_value { self.count } else { self.count - 1 };
self.button_down = if self.current_index == last {
ButtonState::Inactive
} else {
ButtonState::Normal
};
} else {
self.button_down = ButtonState::Inactive;
}
}
fn move_scrollview_up(&mut self) {
let cpoz = self.start_index;
if self.start_index > 0 {
self.start_index -= 1;
self.update_current_index(self.current_index);
}
if (cpoz == self.start_index) && (self.current_index > 0) {
self.update_current_index(self.current_index - 1);
if self.start_index > 0 {
self.start_index -= 1;
self.update_current_index(self.current_index);
}
}
}
fn move_scrollview_down(&mut self) {
let cpoz = self.start_index;
self.start_index += 1;
self.update_current_index(self.current_index);
if cpoz == self.start_index {
self.update_current_index(self.current_index + 1);
self.start_index += 1;
self.update_current_index(self.current_index);
}
}
fn find_first_with_letter(&self, data: &T, to_find: char) -> u32 {
if self.count == 0 {
return u32::MAX;
}
for i in 1..=self.count {
let poz = (self.current_index + i) % self.count;
if let Some(name) = data.name(poz) {
if name
.chars()
.next()
.map(|c| c.eq_ignore_ascii_case(&to_find))
.unwrap_or(false)
{
return poz;
}
}
}
u32::MAX
}
fn mouse_pos_to_index(&self, x: i32, y: i32) -> u32 {
if self.expanded_size.height == 0 {
return u32::MAX;
}
let size = self.expanded_size;
let visible_items = (if size.height > 3 { size.height - 3 } else { 1 }) as i32;
if (x > 0) && (x < size.width as i32) && (y > self.expanded_panel_y) && (y <= self.expanded_panel_y + visible_items) {
return self.start_index + (y - (self.expanded_panel_y + 1)) as u32;
}
u32::MAX
}
#[inline(always)]
fn update_position_from_invalid_index(&mut self) -> bool {
if self.count == 0 {
return true;
}
if self.allow_none_value {
return false;
}
if self.current_index >= self.count {
self.update_current_index(0);
return true;
}
false
}
fn is_on_button_up(&self, x: i32, y: i32) -> bool {
if self.button_up == ButtonState::Hidden {
return false;
}
let x_start_menu = (self.expanded_size.width / 2) as i32 - 1;
(y == self.expanded_panel_y) && (x >= x_start_menu) && (x < x_start_menu + 3)
}
fn is_on_buttom_down(&self, x: i32, y: i32) -> bool {
if self.button_down == ButtonState::Hidden {
return false;
}
let x_start_menu = (self.expanded_size.width / 2) as i32 - 1;
(y == self.expanded_panel_y + self.expanded_size.height as i32 - 2) && (x >= x_start_menu) && (x < x_start_menu + 3)
}
fn paint_buttons(&self, surface: &mut Surface, theme: &Theme) {
let x = (self.expanded_size.width / 2) as i32 - 1;
let attr = self.button_up.color(theme);
surface.fill_horizontal_line_with_size(x, self.expanded_panel_y, 3, Character::with_attributes(' ', attr));
surface.write_char(x + 1, self.expanded_panel_y, Character::with_attributes(SpecialChar::TriangleUp, attr));
let y = self.expanded_panel_y + self.expanded_size.height as i32 - 2;
let attr = self.button_down.color(theme);
surface.fill_horizontal_line_with_size(x, y, 3, Character::with_attributes(' ', attr));
surface.write_char(x + 1, y, Character::with_attributes(SpecialChar::TriangleDown, attr));
}
pub(crate) fn on_paint(&self, control: &ControlBase, data: &T, surface: &mut Surface, theme: &Theme) {
let size = control.size();
let col_text = match () {
_ if !control.is_enabled() => theme.button.regular.text.inactive,
_ if control.has_focus() => theme.button.regular.text.focused,
_ if control.is_mouse_over() => theme.button.regular.text.hovered,
_ => theme.button.regular.text.normal,
};
let space_char = Character::with_attributes(' ', col_text);
if size.width > MINSPACE_FOR_DRAWING {
surface.fill_horizontal_line(0, self.header_y_ofs, (size.width - MINSPACE_FOR_DRAWING) as i32, space_char);
if size.width > MIN_WIDTH_VARIANT_NAME {
let mut paint_next = true;
let mut format = TextFormatBuilder::new()
.position(1, self.header_y_ofs)
.attribute(col_text)
.align(TextAlignment::Left)
.build();
if self.symbol_size > 0 {
format.set_wrap_type(WrapType::SingleLineWrap(self.symbol_size as u16));
if let Some(value) = data.symbol(self.current_index) {
surface.write_text(value, &format);
}
format.x += self.symbol_size as i32;
format.x += 1;
let w = (size.width - MIN_WIDTH_VARIANT_NAME) as i32 - self.symbol_size as i32 - 1;
if w > 0 {
format.set_wrap_type(WrapType::SingleLineWrap(w as u16));
} else {
paint_next = false;
}
} else {
format.set_wrap_type(WrapType::SingleLineWrap((size.width - MIN_WIDTH_VARIANT_NAME) as u16));
}
if paint_next {
if let Some(value) = data.name(self.current_index) {
surface.write_text(value, &format);
} else if !self.none_repr.is_empty() {
surface.write_text(&self.none_repr, &format);
}
}
}
}
if size.width >= MINSPACE_FOR_DROPBUTTON_DRAWING {
let px = (size.width - MINSPACE_FOR_DROPBUTTON_DRAWING) as i32;
surface.fill_horizontal_line_with_size(px, self.header_y_ofs, 3, space_char);
surface.write_char(px + 1, self.header_y_ofs, Character::with_attributes(SpecialChar::TriangleDown, col_text));
}
if control.is_expanded() {
let size = self.expanded_size;
let col = theme.menu.text.normal;
surface.fill_rect(
Rect::with_size(0, self.expanded_panel_y, size.width as u16, (size.height - 1) as u16),
Character::with_attributes(' ', col),
);
surface.draw_rect(
Rect::with_size(0, self.expanded_panel_y, size.width as u16, (size.height - 1) as u16),
LineType::Single,
col,
);
if self.button_up != ButtonState::Hidden {
self.paint_buttons(surface, theme);
}
if (self.count > 0) && (size.height > 3) {
let visible_items = size.height - 3;
let mut display_value = true;
let mut format = TextFormatBuilder::new()
.position(2, self.expanded_panel_y + 1)
.attribute(col_text)
.align(TextAlignment::Left)
.build();
let mut format_symbol = TextFormatBuilder::new()
.position(2, self.expanded_panel_y + 1)
.attribute(theme.menu.symbol.normal)
.align(TextAlignment::Left)
.wrap_type(WrapType::SingleLineWrap(self.symbol_size as u16))
.build();
if self.symbol_size > 0 {
format.x += self.symbol_size as i32;
format.x += 1;
let space_left = (size.width as i32) - (5 + self.symbol_size as i32);
if space_left > 0 {
format.set_wrap_type(WrapType::SingleLineWrap(space_left as u16));
} else {
format.set_wrap_type(WrapType::SingleLineWrap(0));
display_value = false;
}
} else {
format.set_wrap_type(WrapType::SingleLineWrap((size.width - 4) as u16));
}
for i in self.start_index..self.start_index + visible_items {
if self.symbol_size > 0 {
if let Some(value) = data.symbol(i) {
surface.write_text(value, &format_symbol);
}
}
if display_value {
if let Some(value) = data.name(i) {
format.char_attr = theme.menu.text.normal;
surface.write_text(value, &format);
if self.show_description {
if let Some(desc) = data.description(i) {
if !desc.is_empty() {
let sz = value.chars().count();
let old_width = format.width();
let old_x = format.x;
format.x += 1 + sz as i32;
if format.x < (size.width as i32) - 2 {
format.set_wrap_type(WrapType::SingleLineWrap((size.width as i32 - (2 + format.x)) as u16));
format.char_attr = theme.menu.text.inactive;
surface.write_text(desc, &format);
}
format.set_wrap_type(WrapType::SingleLineWrap(old_width));
format.x = old_x;
}
}
}
} else if !self.none_repr.is_empty() {
format.char_attr = theme.menu.text.inactive;
surface.write_text(&self.none_repr, &format);
}
}
if i == self.current_index {
surface.fill_horizontal_line(
1,
format.y,
(size.width - 2) as i32,
Character::with_attributes(0, theme.menu.text.pressed_or_selected),
);
} else if i == self.mouse_index {
surface.fill_horizontal_line(
1,
format.y,
(size.width - 2) as i32,
Character::with_attributes(0, theme.menu.text.hovered),
);
}
format.y += 1;
format_symbol.y += 1;
}
}
}
}
pub(crate) fn on_default_action(&mut self, control: &mut ControlBase) {
if control.is_expanded() {
control.pack();
} else {
let w = control.size().width;
let h = if self.allow_none_value { self.count + 4 } else { self.count + 3 };
control.expand(Size::new(w, h.min(4)), Size::new(w, h));
}
}
pub(crate) fn on_key_pressed(&mut self, control: &mut ControlBase, data: &T, key: Key, character: char) -> EventProcessStatus {
let expanded = control.is_expanded();
match key.value() {
key!("Escape") => {
if expanded {
control.pack();
return EventProcessStatus::Processed;
} else {
return EventProcessStatus::Ignored;
}
}
key!("Space") | key!("Enter") => {
self.on_default_action(control);
return EventProcessStatus::Processed;
}
key!("Up") => {
if self.update_position_from_invalid_index() {
return EventProcessStatus::Processed;
}
if self.current_index > 0 {
self.update_current_index(self.current_index - 1);
};
return EventProcessStatus::Processed;
}
key!("Down") => {
if self.update_position_from_invalid_index() {
return EventProcessStatus::Processed;
}
self.update_current_index(self.current_index + 1);
return EventProcessStatus::Processed;
}
key!("Ctrl+Up") => {
if self.update_position_from_invalid_index() {
return EventProcessStatus::Processed;
}
if expanded {
self.move_scrollview_up();
return EventProcessStatus::Processed;
} else {
return EventProcessStatus::Ignored;
}
}
key!("Ctrl+Down") => {
if self.update_position_from_invalid_index() {
return EventProcessStatus::Processed;
}
if expanded {
self.move_scrollview_down();
return EventProcessStatus::Processed;
} else {
return EventProcessStatus::Ignored;
}
}
key!("Home") => {
self.update_current_index(0);
return EventProcessStatus::Processed;
}
key!("End") => {
self.update_current_index(u32::MAX);
return EventProcessStatus::Processed;
}
key!("PageUp") => {
if self.update_position_from_invalid_index() {
return EventProcessStatus::Processed;
}
let page_count = self.visible_items();
if self.current_index > page_count {
self.update_current_index(self.current_index - page_count);
} else {
self.update_current_index(0);
}
return EventProcessStatus::Processed;
}
key!("PageDown") => {
if self.update_position_from_invalid_index() {
return EventProcessStatus::Processed;
}
self.update_current_index(self.current_index + self.visible_items());
return EventProcessStatus::Processed;
}
_ => {}
}
if !key.modifier.contains_one(KeyModifier::Alt | KeyModifier::Ctrl) {
match character {
'A'..='Z' | 'a'..='z' | '0'..='9' => {
let poz = self.find_first_with_letter(data, character);
if poz != u32::MAX {
self.update_current_index(poz);
}
return EventProcessStatus::Processed;
}
_ => {}
}
}
EventProcessStatus::Ignored
}
pub(crate) fn on_mouse_event(&mut self, control: &mut ControlBase, data: &T, event: &MouseEvent) -> EventProcessStatus {
match event {
MouseEvent::Enter => {
if !control.is_expanded() {
if let Some(desc) = data.description(self.current_index) {
if !desc.is_empty() {
control.show_tooltip(desc);
}
}
}
EventProcessStatus::Processed
}
MouseEvent::Leave => {
control.hide_tooltip();
self.mouse_index = u32::MAX;
EventProcessStatus::Processed
}
MouseEvent::Over(p) => {
let idx = self.mouse_pos_to_index(p.x, p.y);
let is_on_button_up = self.is_on_button_up(p.x, p.y);
let is_on_button_down = self.is_on_buttom_down(p.x, p.y);
let mut result = EventProcessStatus::Ignored;
if self.button_up.is_accesible() && self.button_up.update_state(is_on_button_up, ButtonState::Hovered) {
result = EventProcessStatus::Processed;
}
if self.button_down.is_accesible() && self.button_down.update_state(is_on_button_down, ButtonState::Hovered) {
result = EventProcessStatus::Processed;
}
if idx != self.mouse_index {
self.mouse_index = idx;
result = EventProcessStatus::Processed;
}
result
}
MouseEvent::DoubleClick(data) | MouseEvent::Pressed(data) => {
let is_on_button_up = self.is_on_button_up(data.x, data.y);
let is_on_button_down = self.is_on_buttom_down(data.x, data.y);
if is_on_button_up {
if self.button_up.is_accesible() && self.current_index > 0 {
self.update_current_index(self.current_index - 1);
};
if self.button_up.is_accesible() {
self.button_up = ButtonState::Pressed;
}
return EventProcessStatus::Processed;
}
if is_on_button_down {
if self.button_down.is_accesible() {
self.update_current_index(self.current_index + 1);
}
if self.button_down.is_accesible() {
self.button_down = ButtonState::Pressed;
}
return EventProcessStatus::Processed;
}
let idx = self.mouse_pos_to_index(data.x, data.y);
if idx != u32::MAX {
self.update_current_index(idx);
}
self.on_default_action(control);
EventProcessStatus::Processed
}
MouseEvent::Released(data) => {
let is_on_button_up = self.is_on_button_up(data.x, data.y);
let is_on_button_down = self.is_on_buttom_down(data.x, data.y);
let mut result = EventProcessStatus::Ignored;
if self.button_up.is_accesible() && self.button_up.update_state(is_on_button_up, ButtonState::Hovered) {
result = EventProcessStatus::Processed;
}
if self.button_down.is_accesible() && self.button_down.update_state(is_on_button_down, ButtonState::Hovered) {
result = EventProcessStatus::Processed;
}
result
}
MouseEvent::Wheel(direction) => {
match direction {
MouseWheelDirection::Up => {
if self.current_index > 0 {
self.update_current_index(self.current_index - 1);
}
}
MouseWheelDirection::Down => self.update_current_index(self.current_index + 1),
_ => {}
}
EventProcessStatus::Processed
}
_ => EventProcessStatus::Ignored,
}
}
pub(crate) fn on_expand(&mut self, control: &mut ControlBase, direction: ExpandedDirection) {
match direction {
ExpandedDirection::OnTop => {
self.expanded_panel_y = 0;
self.header_y_ofs = (control.expanded_size().height as i32) - 1;
}
ExpandedDirection::OnBottom => {
self.expanded_panel_y = 1;
self.header_y_ofs = 0;
}
}
self.expanded_size = control.expanded_size();
if self.current_index < self.count {
self.update_current_index(self.current_index);
}
self.mouse_index = u32::MAX;
let items_to_show = if self.allow_none_value { self.count + 1 } else { self.count };
if self.expanded_size.height == items_to_show + 3 {
self.button_down = ButtonState::Hidden;
self.button_up = ButtonState::Hidden;
} else {
self.button_down = ButtonState::Normal;
self.button_up = ButtonState::Normal;
self.update_button_states();
}
}
pub(crate) fn on_pack(&mut self) {
self.expanded_panel_y = 1;
self.header_y_ofs = 0;
self.mouse_index = u32::MAX;
self.expanded_size = Size::default();
self.button_down = ButtonState::Hidden;
self.button_up = ButtonState::Hidden;
}
pub(crate) fn set_none_string(&mut self, value: &str) {
self.none_repr.clear();
self.none_repr.push_str(value);
}
pub(crate) fn update_count(&mut self, control: &mut ControlBase, new_count: u32) {
if new_count == self.count {
return;
}
if new_count > self.count {
if self.current_index >= self.count {
self.current_index = new_count;
}
} else if self.current_index >= new_count {
self.current_index = new_count;
}
self.start_index = self.start_index.min(if new_count > 0 { new_count - 1 } else { 0 });
self.count = new_count;
if self.expanded_size.height > 0 {
control.pack();
}
}
}