diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 121920b9..93c27e41 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -3,6 +3,8 @@ //! Cosmic-themed widget implementations. +pub mod aspect_ratio; + mod button; pub use button::*; @@ -21,8 +23,9 @@ pub use nav_bar::nav_bar; pub mod nav_bar_toggle; pub use nav_bar_toggle::{nav_bar_toggle, NavBarToggle}; -mod toggler; -pub use toggler::toggler; +pub mod rectangle_tracker; + +pub mod search; pub mod segmented_button; pub use segmented_button::horizontal as horizontal_segmented_button; @@ -37,15 +40,14 @@ pub mod settings; mod scrollable; pub use scrollable::*; -mod text; -pub use text::{text, Text}; - pub mod spin_button; pub use spin_button::{spin_button, SpinButton}; -pub mod rectangle_tracker; +mod text; +pub use text::{text, Text}; -pub mod aspect_ratio; +mod toggler; +pub use toggler::toggler; pub mod view_switcher; pub use view_switcher::horizontal as horiontal_view_switcher; diff --git a/src/widget/search/field.rs b/src/widget/search/field.rs new file mode 100644 index 00000000..463c9ecf --- /dev/null +++ b/src/widget/search/field.rs @@ -0,0 +1,89 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::iced::{ + self, + widget::{container, Button}, + Background, Length, +}; +use crate::Renderer; +use apply::Apply; + +/// A search field for COSMIC applications. +pub fn field( + id: iced::widget::text_input::Id, + phrase: &str, + on_change: fn(String) -> Message, + on_clear: Message, + on_submit: Option, +) -> Field { + Field { + id, + phrase, + on_change, + on_clear, + on_submit, + } +} + +/// A search field for COSMIC applications. +#[must_use] +pub struct Field<'a, Message: 'static + Clone> { + id: iced::widget::text_input::Id, + phrase: &'a str, + on_change: fn(String) -> Message, + on_clear: Message, + on_submit: Option, +} + +impl<'a, Message: 'static + Clone> Field<'a, Message> { + pub fn into_element(mut self) -> crate::Element<'a, Message> { + let mut input = iced::widget::text_input("", self.phrase, self.on_change) + .style(crate::theme::TextInput::Search) + .width(Length::Fill) + .id(self.id); + + if let Some(message) = self.on_submit.take() { + input = input.on_submit(message); + } + + iced::widget::row!( + super::icon::search(16), + input, + clear_button().on_press(self.on_clear) + ) + .width(Length::Units(300)) + .height(Length::Units(38)) + .padding([0, 16]) + .spacing(8) + .align_items(iced::Alignment::Center) + .apply(container) + .style(crate::theme::Container::Custom(active_style)) + .into() + } +} + +impl<'a, Message: 'static + Clone> From> for crate::Element<'a, Message> { + fn from(field: Field<'a, Message>) -> Self { + field.into_element() + } +} + +fn clear_button() -> Button<'static, Message, Renderer> { + super::icon::edit_clear(16) + .style(crate::theme::Svg::Symbolic) + .apply(iced::widget::button) + .style(crate::theme::Button::Text) +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn active_style(theme: &crate::Theme) -> container::Appearance { + let cosmic = &theme.cosmic(); + iced::widget::container::Appearance { + text_color: Some(cosmic.primary.on.into()), + background: Some(Background::Color(cosmic.secondary.component.divider.into())), + border_radius: 24.0, + border_width: 2.0, + border_color: cosmic.accent.focus.into(), + } +} diff --git a/src/widget/search/mod.rs b/src/widget/search/mod.rs new file mode 100644 index 00000000..61c83a32 --- /dev/null +++ b/src/widget/search/mod.rs @@ -0,0 +1,116 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! A COSMIC search widget +//! +//! ## Example +//! +//! Store the model in the application: +//! +//! ```ignore +//! App { +//! search: search::Model::default() +//! } +//! ``` +//! +//! Generate the element in the view: +//! +//! ```ignore +//! let search_field = search::search(&self.search, Message::Search); +//! ``` +//! +//! Handle messages in the update method: +//! +//! ```ignore +//! match message { +//! Message::Search(search::Message::Activate) => { +//! // Returns command to focus the text input. +//! return self.search.focus(); +//! } +//! Message::Search(search::Message::Changed) => { +//! self.search.phrase = phrase; +//! self.search_changed(); +//! } +//! Message::Search(search::Message::Clear) => { +//! self.search_clear(); +//! }, +//! Message::Search(search::Message::Submit) => { +//! self.search_submit(); +//! } +//! } + +mod field; +mod model; + +mod button { + use crate::iced::{self, widget::container}; + use apply::Apply; + + /// A search button which converts to a search [`field`] on click. + #[must_use] + pub fn button(on_press: Message) -> crate::Element<'static, Message> { + super::icon::search(16) + .style(crate::theme::Svg::SymbolicActive) + .apply(iced::widget::button) + .style(crate::theme::Button::Text) + .on_press(on_press) + .apply(container) + .padding([0, 0, 0, 11]) + .into() + } +} + +pub mod icon { + use crate::widget::IconSource; + + #[must_use] + pub fn search(size: u16) -> crate::widget::Icon<'static> { + crate::widget::icon( + IconSource::svg_from_memory(&include_bytes!("search.svg")[..]), + size, + ) + } + + #[must_use] + pub fn edit_clear(size: u16) -> crate::widget::Icon<'static> { + crate::widget::icon(IconSource::from("edit-clear-symbolic"), size) + } +} + +pub use button::button; +pub use field::{field, Field}; +pub use model::Model; + +/// Creates the COSMIC search field widget +/// +/// A button is displayed when inactive, and the search field when active. +pub fn search(model: &Model, on_emit: fn(Message) -> M) -> crate::Element { + let element = match model.state { + State::Active => field( + model.input_id.clone(), + &model.phrase, + Message::Changed, + Message::Clear, + Some(Message::Clear), + ) + .into(), + + State::Inactive => button(Message::Activate), + }; + + element.map(on_emit) +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Message { + Activate, + Changed(String), + Clear, + Submit, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum State { + Active, + Inactive, +} diff --git a/src/widget/search/model.rs b/src/widget/search/model.rs new file mode 100644 index 00000000..27cafcaa --- /dev/null +++ b/src/widget/search/model.rs @@ -0,0 +1,37 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::State; +use crate::iced; + +/// A model for managing the state of a search widget. +pub struct Model { + pub input_id: iced::widget::text_input::Id, + pub phrase: String, + pub state: State, +} + +impl Model { + /// Focuses the search field. + #[must_use] + pub fn focus(&mut self) -> crate::iced::Command { + self.state = State::Active; + iced::widget::text_input::focus(self.input_id.clone()) + } + + /// Check if the search field is currently active. + #[must_use] + pub fn is_active(&self) -> bool { + self.state == State::Active + } +} + +impl Default for Model { + fn default() -> Self { + Self { + input_id: iced::widget::text_input::Id::unique(), + phrase: String::with_capacity(32), + state: State::Inactive, + } + } +} diff --git a/src/widget/search/search.svg b/src/widget/search/search.svg new file mode 100644 index 00000000..33b8e88e --- /dev/null +++ b/src/widget/search/search.svg @@ -0,0 +1,11 @@ + + + + + + + + + + +