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

1use std::collections::HashMap;
2
3use azalea_service::{LocalListenerHandle, StaticServiceManager};
4use gtk::prelude::*;
5use relm4::{
6    Component, ComponentParts, ComponentSender, component, factory::FactoryHashMap, prelude::*,
7};
8
9use crate::{
10    factory, icon,
11    service::{self, dbus::bluez::Device},
12};
13
14crate::init! {
15    Model {
16        is_powered: bool,
17        devices_menu: FactoryHashMap<String, factory::bluetooth::device::Model>,
18        _event_listener_handle: LocalListenerHandle,
19    }
20
21    Config {}
22}
23
24#[derive(Debug)]
25pub enum Input {
26    Connect(String, bool),
27    Power(bool),
28    Bluez(service::dbus::bluez::Output),
29}
30
31#[derive(Debug)]
32pub enum CommandOutput {
33    SetDevices(HashMap<String, Device>),
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::MenuButton {
45            set_hexpand: false,
46            set_vexpand: false,
47            set_valign: gtk::Align::Center,
48
49            set_direction: gtk::ArrowType::Up,
50
51            #[watch]
52            set_icon_name: if model.is_powered { icon::BLUETOOTH } else { icon::BLUETOOTH_X },
53
54            #[wrap(Some)]
55            set_popover = &gtk::Popover {
56                set_position: gtk::PositionType::Right,
57
58                gtk::Box {
59                    set_orientation: gtk::Orientation::Vertical,
60
61                    gtk::Box {
62                        gtk::Label::new(Some("Bluetooth")) {
63                            inline_css: r#"
64                                font-weight: bold;
65                            "#,
66
67                            #[watch]
68                            set_css_classes: if model.is_powered {
69                                &[ "azalea-primary-fg" ]
70                            } else {
71                                &[]
72                            },
73
74                            set_halign: gtk::Align::Start,
75                            set_hexpand: true,
76                        },
77                        gtk::Switch {
78                            set_halign: gtk::Align::End,
79
80                            #[watch]
81                            #[block_signal(toggle_state)]
82                            set_active: model.is_powered,
83
84                            connect_state_set[sender] => move |_, on| {
85                                sender.input(Input::Power(on));
86                                false.into()
87                            } @toggle_state,
88                        },
89                    },
90
91                    gtk::Separator {},
92
93                    #[local_ref]
94                    devices_widget -> gtk::Box {
95                        set_orientation: gtk::Orientation::Vertical,
96                        set_spacing: 5,
97                    }
98                },
99            },
100        },
101    }
102
103    fn init(
104        _init: Self::Init,
105        _root: Self::Root,
106        sender: ComponentSender<Self>,
107    ) -> ComponentParts<Self> {
108        let model = Model {
109            is_powered: true,
110            devices_menu: FactoryHashMap::builder()
111                .launch(gtk::Box::default())
112                .forward(sender.input_sender(), |output| match output {
113                    factory::bluetooth::device::Output::Connect(device, connect) => {
114                        Input::Connect(device.address, connect)
115                    }
116                }),
117            _event_listener_handle: service::dbus::bluez::Service::forward_local(
118                sender.input_sender().clone(),
119                Input::Bluez,
120            ),
121        };
122
123        let (tx, rx) = flume::bounded(1);
124        service::dbus::bluez::Service::send(service::dbus::bluez::Input::Devices(tx));
125        sender.oneshot_command(async move {
126            let devices = rx.recv_async().await.unwrap();
127            CommandOutput::SetDevices(devices)
128        });
129
130        let devices_widget = model.devices_menu.widget();
131        let widgets = view_output!();
132
133        ComponentParts { model, widgets }
134    }
135
136    fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>, _root: &Self::Root) {
137        match message {
138            Input::Connect(address, connect) => {
139                service::dbus::bluez::Service::send(service::dbus::bluez::Input::Connect(
140                    address, connect,
141                ));
142            }
143            Input::Bluez(output) => match output {
144                service::dbus::bluez::Output::Connected(device_address, connected) => {
145                    if let Some(mut menu_entry) = self.devices_menu.get_mut(&device_address) {
146                        menu_entry.device.is_connected = connected;
147                    }
148                }
149                service::dbus::bluez::Output::Powered(on) => {
150                    self.is_powered = on;
151                }
152            },
153            Input::Power(on) => {
154                service::dbus::bluez::Service::send(service::dbus::bluez::Input::Power(on));
155            }
156        }
157    }
158
159    fn update_cmd(
160        &mut self,
161        message: Self::CommandOutput,
162        _sender: ComponentSender<Self>,
163        _root: &Self::Root,
164    ) {
165        match message {
166            CommandOutput::SetDevices(devices) => {
167                self.devices_menu.clear();
168                for (address, device) in devices.into_iter() {
169                    self.devices_menu.insert(address, device);
170                }
171            }
172        }
173    }
174}