feat(widget): add COSMIC search widget
This commit is contained in:
parent
ace16b3bc0
commit
a1b3d6ec38
5 changed files with 262 additions and 7 deletions
89
src/widget/search/field.rs
Normal file
89
src/widget/search/field.rs
Normal 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
116
src/widget/search/mod.rs
Normal 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,
|
||||
}
|
||||
37
src/widget/search/model.rs
Normal file
37
src/widget/search/model.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/widget/search/search.svg
Normal file
11
src/widget/search/search.svg
Normal 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 |
Loading…
Add table
Add a link
Reference in a new issue