mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-12-23 10:11:57 +00:00
feat: add search bar to ActionPanel
This commit is contained in:
parent
20b652a30d
commit
a4e33b5a0f
5 changed files with 122 additions and 46 deletions
|
|
@ -1,6 +1,6 @@
|
|||
use iced::{
|
||||
Color, Element, Length,
|
||||
widget::{Button, column, container, mouse_area, opaque, row, text},
|
||||
Border, Color, Element, Length, Theme, color,
|
||||
widget::{Button, column, container, mouse_area, opaque, row, text, text_input},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -14,70 +14,133 @@ const ICON_FONT: iced::Font = iced::Font::with_name("Raycast-Icons");
|
|||
pub enum ActionPanelMessage {
|
||||
Close,
|
||||
InvokeAction(crate::components::actions::ActionHandler),
|
||||
SearchChanged(String),
|
||||
}
|
||||
|
||||
pub fn render_action_panel(actions: &[ActionPanelItem]) -> Element<'_, ActionPanelMessage> {
|
||||
let actions_col = actions
|
||||
.iter()
|
||||
pub fn render_action_panel(
|
||||
actions: &[ActionPanelItem],
|
||||
search_text: &str,
|
||||
) -> Element<'static, ActionPanelMessage> {
|
||||
let filtered_actions = filter_actions(actions, search_text);
|
||||
let search_text_owned = search_text.to_string();
|
||||
|
||||
let actions_col = filtered_actions
|
||||
.into_iter()
|
||||
.fold(column![].spacing(10), |col, action| {
|
||||
col.push(match action {
|
||||
ActionPanelItem::Action(action) => render_action(action),
|
||||
ActionPanelItem::Section(section) => render_section(section),
|
||||
ActionPanelItem::Action(action) => render_action_owned(action),
|
||||
ActionPanelItem::Section(section) => render_section_owned(section),
|
||||
})
|
||||
});
|
||||
|
||||
let search_bar = text_input("Search for actions...", &search_text_owned)
|
||||
.on_input(ActionPanelMessage::SearchChanged)
|
||||
.size(14)
|
||||
.padding(8)
|
||||
.style(|_theme: &Theme, _status| text_input::Style {
|
||||
background: iced::Background::Color(Color::TRANSPARENT),
|
||||
border: iced::Border::default(),
|
||||
icon: Color::from_rgba(1.0, 1.0, 1.0, 0.6),
|
||||
placeholder: Color::from_rgba(1.0, 1.0, 1.0, 0.4),
|
||||
value: Color::WHITE,
|
||||
selection: Color::from_rgba(1.0, 1.0, 1.0, 0.2),
|
||||
});
|
||||
|
||||
let action_panel = column![
|
||||
container(opaque(column![actions_col, search_bar,]))
|
||||
.padding(8)
|
||||
.style(|_theme| {
|
||||
container::Style {
|
||||
background: Some(color!(0x2c2c2c).into()), // TODO: where does this color come from?
|
||||
border: Border::default().rounded(8.0),
|
||||
..Default::default()
|
||||
}
|
||||
}),
|
||||
container(column![]).height(40)
|
||||
]
|
||||
.width(Length::Fixed(368.0))
|
||||
.spacing(10);
|
||||
|
||||
opaque(
|
||||
mouse_area(
|
||||
container(
|
||||
column![
|
||||
container(opaque(actions_col))
|
||||
.padding(8)
|
||||
.style(|_theme| container::Style {
|
||||
background: Some(Color::from_rgba(0.1, 0.1, 0.1, 0.95).into()),
|
||||
..Default::default()
|
||||
}),
|
||||
container(column![]).height(40)
|
||||
]
|
||||
.spacing(10),
|
||||
)
|
||||
.align_bottom(Length::Fill)
|
||||
.align_right(Length::Fill),
|
||||
container(action_panel)
|
||||
.align_bottom(Length::Fill)
|
||||
.align_right(Length::Fill),
|
||||
)
|
||||
.on_press(ActionPanelMessage::Close),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_action(action: &Action) -> Element<'_, ActionPanelMessage> {
|
||||
let mut button = Button::new(
|
||||
if let Some(icon) = action
|
||||
.icon
|
||||
.as_ref()
|
||||
.and_then(|icon_name| icons::get_icon(icon_name))
|
||||
{
|
||||
row![text(icon).font(ICON_FONT), text(action.title.clone())].into()
|
||||
} else {
|
||||
Element::from(text(action.title.clone()))
|
||||
},
|
||||
);
|
||||
fn filter_actions(actions: &[ActionPanelItem], search_text: &str) -> Vec<ActionPanelItem> {
|
||||
if search_text.is_empty() {
|
||||
return actions.to_vec();
|
||||
}
|
||||
|
||||
if let Some(handler) = &action.handler {
|
||||
button = button.on_press(ActionPanelMessage::InvokeAction(handler.clone()));
|
||||
let search_lower = search_text.to_lowercase();
|
||||
|
||||
actions
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
ActionPanelItem::Action(action) => {
|
||||
if action.title.to_lowercase().contains(&search_lower) {
|
||||
Some(ActionPanelItem::Action(action.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ActionPanelItem::Section(section) => {
|
||||
let filtered_children: Vec<Action> = section
|
||||
.children
|
||||
.iter()
|
||||
.filter(|a| a.title.to_lowercase().contains(&search_lower))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if filtered_children.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ActionPanelItem::Section(ActionPanelSection {
|
||||
props: section.props.clone(),
|
||||
children: filtered_children,
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_action_owned(action: Action) -> Element<'static, ActionPanelMessage> {
|
||||
let title = action.title.clone();
|
||||
let icon_char = action
|
||||
.icon
|
||||
.as_ref()
|
||||
.and_then(|icon_name| icons::get_icon(icon_name))
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let mut button = Button::new(if let Some(icon) = icon_char {
|
||||
row![text(icon).font(ICON_FONT), text(title)].into()
|
||||
} else {
|
||||
Element::from(text(title))
|
||||
});
|
||||
|
||||
if let Some(handler) = action.handler {
|
||||
button = button.on_press(ActionPanelMessage::InvokeAction(handler));
|
||||
}
|
||||
|
||||
button.into()
|
||||
}
|
||||
|
||||
fn render_section(section: &ActionPanelSection) -> Element<'_, ActionPanelMessage> {
|
||||
let section_title = text(§ion.props.title)
|
||||
fn render_section_owned(section: ActionPanelSection) -> Element<'static, ActionPanelMessage> {
|
||||
let section_title = text(section.props.title.clone())
|
||||
.size(16)
|
||||
.color(Color::from_rgb8(0xFF, 0xFF, 0xFF));
|
||||
|
||||
let section_actions = section
|
||||
.children
|
||||
.iter()
|
||||
.into_iter()
|
||||
.fold(column![].spacing(5), |col, action| {
|
||||
col.push(render_action(action))
|
||||
col.push(render_action_owned(action))
|
||||
});
|
||||
|
||||
column![section_title, section_actions].spacing(5).into()
|
||||
|
|
@ -87,5 +150,6 @@ pub fn map_action_panel_message(msg: ActionPanelMessage) -> crate::Message {
|
|||
match msg {
|
||||
ActionPanelMessage::Close => crate::Message::ToggleActionPanel(false),
|
||||
ActionPanelMessage::InvokeAction(handler) => crate::Message::InvokeAction(handler),
|
||||
ActionPanelMessage::SearchChanged(text) => crate::Message::ActionPanelSearchChanged(text),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ pub enum Message {
|
|||
ImageLoaded(String, Handle),
|
||||
InvokeAction(ActionHandler),
|
||||
ToggleActionPanel(bool),
|
||||
ActionPanelSearchChanged(String),
|
||||
ShowToast(String),
|
||||
DropdownChanged(String),
|
||||
LaunchCommand(ExtensionCommand),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ pub struct State {
|
|||
pub search_text: String,
|
||||
pub search_input_id: WidgetId,
|
||||
pub action_panel_visible: bool,
|
||||
pub action_panel_search: String,
|
||||
pub selected_actions: Vec<ActionPanelItem>,
|
||||
pub toast_message: String,
|
||||
pub window_id: Option<window::Id>,
|
||||
|
|
@ -63,6 +64,7 @@ impl State {
|
|||
search_text,
|
||||
search_input_id: WidgetId::unique(),
|
||||
action_panel_visible: false,
|
||||
action_panel_search: String::new(),
|
||||
selected_actions: Vec::new(),
|
||||
toast_message: String::new(),
|
||||
window_id: None,
|
||||
|
|
|
|||
|
|
@ -106,10 +106,16 @@ pub fn update(state: &mut State, message: Message) -> Task<Message> {
|
|||
state.action_panel_visible = visibility;
|
||||
if visibility {
|
||||
return operation::focus_next();
|
||||
} else if state.screen.can_search() {
|
||||
return operation::focus(state.search_input_id.clone());
|
||||
} else {
|
||||
state.action_panel_search.clear();
|
||||
if state.screen.can_search() {
|
||||
return operation::focus(state.search_input_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::ActionPanelSearchChanged(text) => {
|
||||
state.action_panel_search = text;
|
||||
}
|
||||
Message::ShowToast(message) => {
|
||||
state.toast_message = message;
|
||||
}
|
||||
|
|
|
|||
13
src/view.rs
13
src/view.rs
|
|
@ -2,9 +2,9 @@ use iced::widget::{column, container, pick_list, row, rule, stack, text_input};
|
|||
use iced::{Element, Length, Theme};
|
||||
|
||||
use crate::components::{
|
||||
action_panel::{render_action_panel, map_action_panel_message},
|
||||
action_panel::{map_action_panel_message, render_action_panel},
|
||||
dropdown::{Dropdown, DropdownChild},
|
||||
footer::{render_footer, map_footer_message},
|
||||
footer::{map_footer_message, render_footer},
|
||||
};
|
||||
use crate::message::Message;
|
||||
use crate::screens::{Screen, Shell};
|
||||
|
|
@ -37,15 +37,18 @@ pub fn view(state: &State) -> Element<'_, Message> {
|
|||
Screen::List(s) => s.view().map(Message::List),
|
||||
};
|
||||
|
||||
let footer = render_footer(&state.selected_actions, &state.toast_message, theme)
|
||||
.map(map_footer_message);
|
||||
let footer =
|
||||
render_footer(&state.selected_actions, &state.toast_message, theme).map(map_footer_message);
|
||||
|
||||
base_col = base_col
|
||||
.push(container(content).width(Length::Fill).height(Length::Fill))
|
||||
.push(footer);
|
||||
|
||||
let action_panel = if state.action_panel_visible {
|
||||
Some(render_action_panel(&state.selected_actions).map(map_action_panel_message))
|
||||
Some(
|
||||
render_action_panel(&state.selected_actions, &state.action_panel_search)
|
||||
.map(map_action_panel_message),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue