azalea_shell/window/taskbar/widget/search/
mod.rs

1use 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        // TODO: Use this to determine which order display results
20        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= &gtk::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}