azalea_shell/window/taskbar/widget/search/
mod.rs1use azalea_service::{LocalListenerHandle, StaticServiceManager};
2use gtk::{gdk, glib, prelude::*};
3use gtk4_layer_shell::LayerShell;
4use relm4::{Component, ComponentParts, ComponentSender, component, prelude::FactoryVecDeque};
5
6use crate::{
7 factory, icon,
8 service::{self, search::AppInfo},
9};
10
11crate::init! {
12 Model {
13 search: String,
14 apps: FactoryVecDeque<factory::search::apps::Model>,
15 _service_handle: LocalListenerHandle,
16 }
17
18 Config {
19 top_down: bool,
21 }
22}
23
24#[derive(Debug)]
25pub enum Input {
26 Search(String),
27 SelectFirst,
28 SearchResults(service::search::Output),
29}
30
31#[derive(Debug)]
32pub enum CommandOutput {
33 SetApplications(Vec<AppInfo>),
34}
35
36#[component(pub)]
37impl Component for Model {
38 type Init = Init;
39 type Input = Input;
40 type Output = ();
41 type CommandOutput = CommandOutput;
42
43 view! {
44 gtk::Button {
45 set_valign: gtk::Align::Center,
46 #[watch]
47 set_css_classes: if model.search.len() > 0 {
48 &[
49 "azalea-primary-container",
50 "azalea-circle-bubble",
51 "azalea-primary-border",
52 "azalea-secondary-container-hover",
53 ]
54 } else { &[] },
55
56 #[wrap(Some)]
57 set_child= >k::Box {
58 set_spacing: 8,
59 set_valign: gtk::Align::Center,
60
61 gtk::Image {
62 set_icon_name: Some(icon::SEARCH),
63 },
64
65 gtk::Label {
66 #[watch]
67 set_label: &model.search,
68 },
69 },
70
71 connect_clicked => move |_| {
72 window.set_visible(!window.get_visible());
73 },
74 }
75 }
76
77 fn init(
78 _init: Self::Init,
79 _root: Self::Root,
80 sender: ComponentSender<Self>,
81 ) -> ComponentParts<Self> {
82 let model = Model {
83 search: format!(""),
84 apps: FactoryVecDeque::builder()
85 .launch(gtk::Box::default())
86 .detach(),
87 _service_handle: service::search::Service::forward_local(
88 sender.input_sender().clone(),
89 Input::SearchResults,
90 ),
91 };
92
93 let (tx, rx) = flume::bounded(1);
94 service::search::Service::send(service::search::Input::GetAllApplications(tx));
95 sender.oneshot_command(async move {
96 let mut applications = rx.recv_async().await.unwrap();
97 applications.sort_by(|a, b| a.name.cmp(&b.name));
98 CommandOutput::SetApplications(applications)
99 });
100
101 let entry = gtk::Entry::new();
102 let entry_clone = entry.clone();
103 let search_result = model.apps.widget();
104
105 relm4::view! {
106 window = gtk::Window {
107 init_layer_shell: (),
108
109 set_layer: gtk4_layer_shell::Layer::Overlay,
110
111 set_anchor: (gtk4_layer_shell::Edge::Top, true),
112 set_anchor: (gtk4_layer_shell::Edge::Bottom, true),
113 set_anchor: (gtk4_layer_shell::Edge::Left, true),
114 set_anchor: (gtk4_layer_shell::Edge::Right, true),
115
116 set_keyboard_mode: gtk4_layer_shell::KeyboardMode::OnDemand,
117
118 set_visible: false,
119
120 connect_visible_notify => move |this| {
121 if !this.get_visible() {
122 entry_clone.set_text("");
123 }
124 },
125
126 add_controller = gtk::EventControllerKey {
127 connect_key_pressed => move |this, key, _code, _modifier| {
128 match key {
129 gdk::Key::Escape => {
130 if let Some(widget) = this.widget(){
131 widget.set_visible(false);
132 }
133 glib::Propagation::Stop
134 },
135 _ => glib::Propagation::Proceed,
136 }
137 },
138 },
139
140 add_css_class: "azalea-transparent",
141
142 gtk::Box {
143 set_halign: gtk::Align::Center,
144 set_valign: gtk::Align::Center,
145 set_orientation: gtk::Orientation::Vertical,
146 set_spacing: 24,
147
148 set_width_request: 300,
149
150 gtk::Box {
151 set_spacing: 12,
152 set_css_classes: &[
153 "azalea-surface",
154 "azalea-semi-transparent",
155 "azalea-bubble",
156 "azalea-primary-border",
157 "azalea-padding"
158 ],
159
160 gtk::Image {
161 set_icon_name: Some(icon::SEARCH),
162 },
163
164 gtk::Separator {
165 set_orientation: gtk::Orientation::Vertical,
166 },
167
168 #[local_ref]
169 entry -> gtk::Entry {
170 connect_activate => Input::SelectFirst,
171 connect_changed[sender] => move |entry| {
172 sender.input(Input::Search(entry.text().to_string()));
173 },
174 },
175 },
176
177 gtk::ScrolledWindow {
178 set_propagate_natural_width: true,
179 set_propagate_natural_height: true,
180
181 set_css_classes: &[
182 "azalea-surface",
183 "azalea-semi-transparent",
184 "azalea-bubble",
185 "azalea-primary-border",
186 "azalea-padding"
187 ],
188
189 #[local_ref]
190 search_result -> gtk::Box {
191 set_orientation: gtk::Orientation::Vertical,
192 set_spacing: 5,
193 }
194 }
195 }
196 }
197 };
198
199 let widgets = view_output!();
200
201 ComponentParts { model, widgets }
202 }
203
204 fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>, _root: &Self::Root) {
205 match message {
206 Input::Search(message) => {
207 self.search = message.clone();
208 self.apps
209 .broadcast(factory::search::apps::Input::Filter(message));
210 }
211 Input::SearchResults(_output) => todo!(),
212 Input::SelectFirst => todo!(),
213 }
214 }
215
216 fn update_cmd(
217 &mut self,
218 message: Self::CommandOutput,
219 _sender: ComponentSender<Self>,
220 _root: &Self::Root,
221 ) {
222 match message {
223 CommandOutput::SetApplications(app_infos) => {
224 let mut guard = self.apps.guard();
225
226 for app_info in app_infos {
227 guard.push_back(app_info);
228 }
229 }
230 }
231 }
232}