feat(widget): add COSMIC search widget

This commit is contained in:
Michael Aaron Murphy 2023-01-26 22:16:13 +01:00 committed by Michael Murphy
parent ace16b3bc0
commit a1b3d6ec38
5 changed files with 262 additions and 7 deletions

View file

@ -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;

View file

@ -0,0 +1,89 @@
// Copyright 2023 System76 <info@system76.com>
// 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<Message: 'static + Clone>(
id: iced::widget::text_input::Id,
phrase: &str,
on_change: fn(String) -> Message,
on_clear: Message,
on_submit: Option<Message>,
) -> Field<Message> {
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<Message>,
}
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<Field<'a, Message>> for crate::Element<'a, Message> {
fn from(field: Field<'a, Message>) -> Self {
field.into_element()
}
}
fn clear_button<Message: 'static>() -> 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(),
}
}

116
src/widget/search/mod.rs Normal file
View file

@ -0,0 +1,116 @@
// Copyright 2023 System76 <info@system76.com>
// 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<Message: 'static + Clone>(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<M: 'static + Clone>(model: &Model, on_emit: fn(Message) -> M) -> crate::Element<M> {
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,
}

View file

@ -0,0 +1,37 @@
// Copyright 2023 System76 <info@system76.com>
// 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<Message: 'static>(&mut self) -> crate::iced::Command<Message> {
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,
}
}
}

View file

@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_876_27069)">
<path d="M13.42 14.83L13.424 14.826C13.5648 14.9629 13.7429 15.0553 13.9359 15.0917C14.1289 15.128 14.3283 15.1068 14.5093 15.0305C14.6903 14.9543 14.8448 14.8264 14.9536 14.6629C15.0624 14.4994 15.1206 14.3074 15.121 14.111C15.155 13.813 14.968 13.534 14.826 13.392L11.725 10.291L10.322 11.721L13.42 14.83Z" fill="white"/>
<path d="M6.5 1.00293C5.04131 1.00293 3.64236 1.58239 2.61091 2.61384C1.57946 3.64529 1 5.04424 1 6.50293C1 7.96162 1.57946 9.36057 2.61091 10.392C3.64236 11.4235 5.04131 12.0029 6.5 12.0029C7.95869 12.0029 9.35764 11.4235 10.3891 10.392C11.4205 9.36057 12 7.96162 12 6.50293C12 5.04424 11.4205 3.64529 10.3891 2.61384C9.35764 1.58239 7.95869 1.00293 6.5 1.00293ZM6.5 3.00293C7.42826 3.00293 8.3185 3.37168 8.97487 4.02806C9.63125 4.68443 10 5.57467 10 6.50293C10 7.43119 9.63125 8.32143 8.97487 8.9778C8.3185 9.63418 7.42826 10.0029 6.5 10.0029C5.57174 10.0029 4.6815 9.63418 4.02513 8.9778C3.36875 8.32143 3 7.43119 3 6.50293C3 5.57467 3.36875 4.68443 4.02513 4.02806C4.6815 3.37168 5.57174 3.00293 6.5 3.00293Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_876_27069">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB