use crate::core::{Color, HorizontalAlignment, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::capability::coercion::{expect_bool, expect_string, expect_usize};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::metrics::{effective_font, estimate_text_width};
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
use super::{Chip, ChipItem};
const INPUT_ROW_HEIGHT: u32 = 26;
pub struct ChipInput {
base: BaseWidget,
band: Chip,
text: String,
placeholder: String,
separator: char,
max_count: usize,
allow_duplicates: bool,
cursor: usize,
pub chip_committed: Signal1<String>,
pub chip_removed: Signal1<String>,
pub text_changed: Signal1<String>,
pub limit_reached: Signal1<usize>,
}
impl ChipInput {
pub fn new(geometry: Rect) -> Self {
let band = Chip::new(geometry);
Self {
base: BaseWidget::new(WidgetKind::TextEdit, geometry, "ChipInput"),
band,
text: String::new(),
placeholder: String::new(),
separator: ',',
max_count: 0,
allow_duplicates: false,
cursor: 0,
chip_committed: Signal1::new(),
chip_removed: Signal1::new(),
text_changed: Signal1::new(),
limit_reached: Signal1::new(),
}
}
pub fn items(&self) -> &[ChipItem] {
self.band.items()
}
pub fn set_items(&mut self, items: Vec<ChipItem>) {
self.band.set_items(items);
self.base.request_redraw();
}
pub fn chip_band(&self) -> &Chip {
&self.band
}
pub fn chip_band_mut(&mut self) -> &mut Chip {
&mut self.band
}
pub fn text(&self) -> &str {
&self.text
}
pub fn set_text(&mut self, text: impl Into<String>) {
let text = text.into();
if self.text == text {
return;
}
self.text = text;
self.cursor = self.text.chars().count();
self.text_changed.emit(self.text.clone());
self.base.request_redraw();
}
pub fn placeholder(&self) -> &str {
&self.placeholder
}
pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
let placeholder = placeholder.into();
if self.placeholder == placeholder {
return;
}
self.placeholder = placeholder;
self.base.request_redraw();
}
pub fn separator(&self) -> char {
self.separator
}
pub fn set_separator(&mut self, separator: char) {
self.separator = separator;
}
pub fn max_count(&self) -> usize {
self.max_count
}
pub fn set_max_count(&mut self, max_count: usize) {
self.max_count = max_count;
}
pub fn allow_duplicates(&self) -> bool {
self.allow_duplicates
}
pub fn set_allow_duplicates(&mut self, allow: bool) {
self.allow_duplicates = allow;
}
pub fn commit(&mut self) -> usize {
let raw = core::mem::take(&mut self.text);
self.cursor = 0;
let mut added = 0usize;
for token in raw.split(self.separator) {
let label = token.trim();
if label.is_empty() {
continue;
}
if self.max_count > 0 && self.band.items().len() >= self.max_count {
self.limit_reached.emit(self.band.items().len());
break;
}
if !self.allow_duplicates && self.band.items().iter().any(|item| item.label == label) {
continue;
}
let id = self.unique_id(label);
let mut items = self.band.items().to_vec();
items.push(ChipItem::new(id.clone(), label));
self.band.set_items(items);
self.chip_committed.emit(id);
added += 1;
}
self.text_changed.emit(String::new());
self.base.request_redraw();
added
}
fn unique_id(&self, label: &str) -> String {
if !self.band.items().iter().any(|item| item.id == label) {
return label.to_string();
}
let mut suffix = 2usize;
loop {
let candidate = format!("{label}#{suffix}");
if !self.band.items().iter().any(|item| item.id == candidate) {
return candidate;
}
suffix += 1;
}
}
pub fn clear(&mut self) {
let removed: Vec<String> = self.band.items().iter().map(|item| item.id.clone()).collect();
self.band.set_items(Vec::new());
for id in removed {
self.chip_removed.emit(id);
}
self.base.request_redraw();
}
fn remove_last(&mut self) {
let mut items = self.band.items().to_vec();
if let Some(item) = items.pop() {
self.band.set_items(items);
self.chip_removed.emit(item.id);
self.base.request_redraw();
}
}
fn insert(&mut self, text: &str) {
let mut chars: Vec<char> = self.text.chars().collect();
let at = self.cursor.min(chars.len());
chars.splice(at..at, text.chars());
self.text = chars.into_iter().collect();
self.cursor = at + text.chars().count();
self.text_changed.emit(self.text.clone());
self.base.request_redraw();
}
fn backspace(&mut self) {
if self.text.is_empty() {
self.remove_last();
return;
}
let mut chars: Vec<char> = self.text.chars().collect();
if self.cursor == 0 || self.cursor > chars.len() {
return;
}
chars.remove(self.cursor - 1);
self.cursor -= 1;
self.text = chars.into_iter().collect();
self.text_changed.emit(self.text.clone());
self.base.request_redraw();
}
fn band_rect(&self) -> Rect {
let rect = self.geometry();
let input = if rect.height > INPUT_ROW_HEIGHT { INPUT_ROW_HEIGHT } else { rect.height };
Rect::new(rect.x, rect.y, rect.width, rect.height - input)
}
fn input_rect(&self) -> Rect {
let rect = self.geometry();
let height = INPUT_ROW_HEIGHT.min(rect.height);
Rect::new(rect.x, rect.y + rect.height as i32 - height as i32, rect.width, height)
}
}
impl Widget for ChipInput {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn size_hint(&self) -> Size {
Size::new(240, 24 + INPUT_ROW_HEIGHT)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for ChipInput {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"items" => WidgetProperties::get(&self.band, "items"),
"item_count" => Ok(CapabilityValue::UInt(self.band.items().len() as u64)),
"text" => Ok(CapabilityValue::String(self.text.clone())),
"placeholder" => Ok(CapabilityValue::String(self.placeholder.clone())),
"separator" => Ok(CapabilityValue::String(self.separator.to_string())),
"max_count" => Ok(CapabilityValue::UInt(self.max_count as u64)),
"allow_duplicates" => Ok(CapabilityValue::Bool(self.allow_duplicates)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"items" => WidgetProperties::set(&mut self.band, "items", value),
"text" => {
self.set_text(expect_string(value)?);
Ok(())
}
"placeholder" => {
self.set_placeholder(expect_string(value)?);
Ok(())
}
"separator" => {
let token = expect_string(value)?;
let separator = token.chars().next().ok_or(CapabilityAccessError::TypeMismatch)?;
self.set_separator(separator);
Ok(())
}
"max_count" => {
self.set_max_count(expect_usize(value)?);
Ok(())
}
"allow_duplicates" => {
self.set_allow_duplicates(expect_bool(value)?);
Ok(())
}
"item_count" => Err(CapabilityAccessError::ReadOnlyProperty),
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"items",
"item_count",
"text",
"placeholder",
"separator",
"max_count",
"allow_duplicates",
BASE_PROPERTY_NAMES
]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"commit" => {
if self.commit() > 0 {
Ok(())
} else {
Err(CapabilityAccessError::OutOfRange)
}
}
"clear" => {
self.clear();
Ok(())
}
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
impl EventHandler for ChipInput {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
let band = self.band_rect();
if self.band.base().geometry() != band {
self.band.base_mut().set_geometry(band);
}
match event {
Event::TextInput { text } | Event::ImeCommit { text } => {
let filtered: String = text.chars().filter(|ch| !ch.is_control()).collect();
if filtered.is_empty() {
return;
}
if filtered.contains(self.separator) {
let mut pieces = filtered.split(self.separator).peekable();
if let Some(first) = pieces.next() {
if !first.is_empty() {
self.insert(first);
}
}
self.commit();
for piece in pieces {
if !piece.is_empty() {
self.insert(piece);
}
}
return;
}
self.insert(&filtered);
}
Event::KeyPress { key, .. } => {
let pressed = crate::shortcut::Key::from_key_code(*key);
match pressed {
Some(crate::shortcut::Key::Enter) => {
self.commit();
}
Some(crate::shortcut::Key::Backspace) => self.backspace(),
Some(crate::shortcut::Key::Escape) => {
if !self.text.is_empty() {
self.text.clear();
self.cursor = 0;
self.text_changed.emit(String::new());
self.base.request_redraw();
}
}
Some(crate::shortcut::Key::Left) => {
self.cursor = self.cursor.saturating_sub(1);
self.base.request_redraw();
}
Some(crate::shortcut::Key::Right) => {
self.cursor = (self.cursor + 1).min(self.text.chars().count());
self.base.request_redraw();
}
Some(crate::shortcut::Key::Home) => {
self.cursor = 0;
self.base.request_redraw();
}
Some(crate::shortcut::Key::End) => {
self.cursor = self.text.chars().count();
self.base.request_redraw();
}
_ => {}
}
}
Event::MousePress { pos, .. } => {
if band.contains_point(*pos) {
self.band.handle_event(event);
} else if self.input_rect().contains_point(*pos) {
self.base.request_redraw();
}
}
Event::MouseMove { pos } | Event::MouseRelease { pos, .. } => {
if self.band.base().geometry().contains_point(*pos) {
self.band.handle_event(event);
}
}
Event::MouseLeave { .. } => self.band.handle_event(event),
_ => {}
}
}
}
impl Draw for ChipInput {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
if rect.width == 0 || rect.height == 0 {
return;
}
let band = self.band_rect();
if self.band.base().geometry() != band {
self.band.base_mut().set_geometry(band);
}
let input = self.input_rect();
let style = self.base.style();
let theme = crate::style::resolved_theme_style_shared("text_edit");
let surface = style
.background_color
.or_else(|| theme.as_ref().and_then(|t| t.background_color))
.unwrap_or(Color::WHITE);
let border = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.unwrap_or_else(|| Color::border_of(surface));
let ink = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(Color::BLACK);
let font = effective_font(style);
context.face_with_gradient(
rect,
surface,
style.background_gradient.as_ref(),
style.surface.unwrap_or_default(),
style.border_radius.unwrap_or(0),
Color::BLACK,
);
crate::widget::draw::stroke_surface_border(
context,
rect,
self.style().border_radius.unwrap_or(0),
border,
1,
);
self.band.draw(context);
context.draw_line(
Point::new(input.x, input.y),
Point::new(input.x + input.width as i32, input.y),
border,
);
let caret_y = input.y + (input.height as i32 - 14) / 2;
let text =
if self.text.is_empty() { self.placeholder.as_str() } else { self.text.as_str() };
let text_ink = if self.text.is_empty() { ink.with_alpha(120) } else { ink };
context.draw_text(
Point::new(input.x + 6, caret_y),
text,
font,
text_ink,
HorizontalAlignment::Left,
);
let prefix: String = self.text.chars().take(self.cursor).collect();
let caret_x = input.x + 6 + estimate_text_width(&prefix, font, 1.0) as i32;
if self.base.is_enabled() {
context.fill_rect(Rect::new(caret_x, caret_y - 1, 1, 15), ink);
}
}
}