Skip to content

Commit aa4682f

Browse files
committed
add proper support for multiple panels
1 parent e2a9158 commit aa4682f

18 files changed

Lines changed: 454 additions & 240 deletions

File tree

build.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ fn main() {
1111
slint_build::compile_with_config("ui/panels/taskbar/taskbar.slint", config.clone())
1212
.expect("Taskbar build failed");
1313

14+
// Compile music player selector panel
15+
slint_build::compile_with_config(
16+
"ui/panels/media_selector/media_selector.slint",
17+
config.clone(),
18+
)
19+
.expect("Music player selector build failed");
20+
1421
// Add more panels as you create them:
1522
// slint_build::compile_with_config("ui/panels/menu/menu.slint", config.clone()).expect("Menu build failed");
1623
// slint_build::compile_with_config("ui/panels/osd/osd.slint", config.clone()).expect("OSD build failed");

src/main.rs

Lines changed: 16 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,23 @@
44
55
mod event_bus;
66
mod functions;
7+
mod panel_manager;
78
mod panels;
89
mod services;
910

10-
use crate::panels::taskbar::{active_window, distro_icon};
11-
use crate::services::wm::hyprland_wm;
1211
use hyprland::data::{Monitor, Monitors};
1312
use hyprland::shared::HyprData;
14-
use log::{debug, error, info};
15-
use panels::taskbar::events::TaskbarEvent;
16-
use panels::taskbar::taskbar::Taskbar;
17-
use panels::taskbar::{battery, bluetooth, clock, events, media, network, volume, workspaces};
18-
use slint::ComponentHandle;
19-
use spell_framework::{
20-
enchant_spells,
21-
layer_properties::{BoardType, LayerAnchor, LayerType, WindowConf},
22-
slint_adapter::SpellMultiWinHandler,
23-
wayland_adapter::SpellWin,
24-
};
13+
use log::{error, info};
2514
use std::error::Error;
2615

27-
const TASKBAR_HEIGHT: u32 = 48;
28-
const EVENT_POLL_INTERVAL_MS: u64 = 50;
16+
use panel_manager::PanelManager;
2917

30-
fn main() -> Result<(), Box<dyn Error>> {
31-
info!("Starting CapyShell...");
18+
use crate::panels::media_selector::MediaSelectorFactory;
19+
use crate::panels::taskbar::TaskbarFactory;
3220

33-
// Start shared background services ONCE
34-
let service_status = services::start_all();
21+
fn main() -> Result<(), Box<dyn Error>> {
22+
println!("Welcome to CapyShell!"); // TODO: add more info
3523

36-
// Get all monitors
3724
let monitors: Vec<Monitor> = match Monitors::get() {
3825
Ok(monitors) => monitors.iter().cloned().collect(),
3926
Err(e) => {
@@ -42,169 +29,20 @@ fn main() -> Result<(), Box<dyn Error>> {
4229
}
4330
};
4431

45-
if monitors.is_empty() {
46-
error!("No monitors found!");
47-
return Err("No monitors found!".into());
48-
}
49-
50-
debug!("Found {} monitors", monitors.len());
51-
52-
let configs: Vec<(String, WindowConf)> = monitors
53-
.iter()
54-
.map(|monitor| {
55-
let name = format!("taskbar-{}", monitor.name);
56-
let conf = WindowConf::new(
57-
monitor.width as u32,
58-
TASKBAR_HEIGHT,
59-
(
60-
Some(LayerAnchor::TOP | LayerAnchor::LEFT | LayerAnchor::RIGHT),
61-
None,
62-
),
63-
(0, 0, 0, 0),
64-
LayerType::Top,
65-
BoardType::None,
66-
Some(TASKBAR_HEIGHT as i32),
67-
Some(monitor.name.clone()),
68-
);
69-
(name, conf)
70-
})
71-
.collect();
72-
73-
let configs_ref: Vec<(&str, WindowConf)> = configs
74-
.iter()
75-
.map(|(name, conf)| (name.as_str(), conf.clone()))
76-
.collect();
77-
78-
let windows: Vec<SpellWin> = SpellMultiWinHandler::conjure_spells(configs_ref);
79-
80-
debug!("Created {} windows", windows.len());
81-
82-
// create Slint UIs for each window
83-
let mut uis: Vec<Taskbar> = Vec::new();
84-
for (i, _waywin) in windows.iter().enumerate() {
85-
let ui = Taskbar::new()?;
86-
let monitor_name = monitors[i].name.clone();
87-
info!("Taskbar {} assigned to monitor '{}'", i, monitor_name);
88-
89-
// Clock callback
90-
let ui_weak_clock = ui.as_weak();
91-
ui.on_update_clock(move || {
92-
if let Some(ui_handle) = ui_weak_clock.upgrade() {
93-
clock::update_clock(&ui_handle);
94-
}
95-
});
96-
97-
// Workspace click callback
98-
ui.on_workspace_clicked(move |workspace_id| {
99-
workspaces::switch_to_workspace(workspace_id);
100-
});
101-
102-
// Event polling timer - each taskbar subscribes to the broadcast channel
103-
let mut event_rx = events::subscribe();
104-
let ui_weak_events = ui.as_weak();
105-
let monitor_name_for_events = monitor_name.clone();
106-
107-
let event_timer = slint::Timer::default();
108-
event_timer.start(
109-
slint::TimerMode::Repeated,
110-
std::time::Duration::from_millis(EVENT_POLL_INTERVAL_MS),
111-
move || {
112-
let events = events::drain_latest(&mut event_rx);
113-
if events.is_empty() {
114-
return;
115-
}
116-
if let Some(ui) = ui_weak_events.upgrade() {
117-
for event in events {
118-
match event {
119-
TaskbarEvent::Battery(status) => {
120-
battery::update_ui(&ui, &status);
121-
}
122-
TaskbarEvent::Volume(status) => {
123-
volume::update_ui(&ui, &status);
124-
}
125-
TaskbarEvent::Network(status) => {
126-
network::update_ui(&ui, &status);
127-
}
128-
TaskbarEvent::Bluetooth(status) => {
129-
bluetooth::update_ui(&ui, &status);
130-
}
131-
TaskbarEvent::Workspaces(status) => {
132-
workspaces::update_ui(&ui, &status, &monitor_name_for_events);
133-
}
134-
TaskbarEvent::Mpris(data) => {
135-
media::update_ui(&ui, &data);
136-
}
137-
TaskbarEvent::ActiveWindow(data) => {
138-
//? this is actually the impl to get the active window per monitor instead of shared across all monitors
139-
// if ONLY_SHOW_ACTIVE_WINDOW_ON_ACTIVE_MONITOR
140-
// && data.focused_monitor != monitor_name_for_events
141-
// {
142-
// return;
143-
// }
144-
active_window::update_ui(&ui, &data, &monitor_name_for_events);
145-
}
146-
TaskbarEvent::SystemStatus(_data) => {
147-
// TODO: Implement UI update for system status
148-
}
149-
}
150-
}
151-
}
152-
},
153-
);
154-
155-
// Initial state
156-
clock::update_clock(&ui);
157-
158-
distro_icon::update_distro_icon(&ui);
159-
160-
// Battery setup (only if battery is present)
161-
ui.set_has_battery(service_status.has_battery);
162-
if service_status.has_battery {
163-
let initial_status = services::battery::get_status();
164-
battery::update_ui(&ui, &initial_status);
165-
}
166-
167-
// Initial volume state
168-
if let Some(volume_status) = services::volume::get_default_volume() {
169-
volume::update_ui(&ui, &volume_status);
170-
}
171-
172-
// Initial network state
173-
let initial_network = services::network::get_status();
174-
network::update_ui(&ui, &initial_network);
175-
176-
// Bluetooth setup (only if bluetooth adapter is present)
177-
ui.set_has_bluetooth(service_status.has_bluetooth);
178-
if service_status.has_bluetooth {
179-
let initial_bluetooth = services::bluetooth::get_status();
180-
bluetooth::update_ui(&ui, &initial_bluetooth);
181-
}
182-
183-
// Initial workspace state
184-
let initial_workspaces = hyprland_wm::workspaces::get_status(&monitor_name);
185-
workspaces::update_ui(&ui, &initial_workspaces, &monitor_name);
186-
187-
media::attach_callbacks(&ui);
188-
189-
// Init active window
190-
let initial_active_window = hyprland_wm::active_window::get_active_window();
191-
active_window::update_ui(&ui, &initial_active_window, &monitor_name);
32+
// Start background services once before init of panels
33+
let service_status = services::start_all();
19234

193-
// Keep timer alive
194-
std::mem::forget(event_timer);
35+
let mut wm = PanelManager::new();
19536

196-
uis.push(ui);
197-
debug!("Initialized UI for monitor {}", i);
198-
}
37+
let taskbar_factory =
38+
TaskbarFactory::new(service_status.has_battery, service_status.has_bluetooth);
19939

200-
info!("CapyShell running with {} taskbars.", windows.len());
40+
let media_selector_factory = MediaSelectorFactory::new();
20141

202-
// Run all windows in single-threaded event loop
203-
let num_windows = windows.len();
204-
let states: Vec<_> = (0..num_windows).map(|_| None).collect();
205-
let callbacks: Vec<_> = (0..num_windows).map(|_| None).collect();
42+
wm.register_factory(taskbar_factory);
43+
wm.register_factory(media_selector_factory);
20644

207-
enchant_spells(windows, states, callbacks)?;
45+
wm.start(&monitors)?;
20846

20947
Ok(())
21048
}

src/panel_manager/factory.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use crate::panel_manager::{PanelInstance, WindowConf};
2+
use hyprland::data::Monitor;
3+
use std::error::Error;
4+
5+
/// Trait that defines a factory for creating a specific type of window (e.g., Taskbar, Launcher).
6+
pub trait PanelFactory {
7+
/// Unique identifier for this window type.
8+
fn type_id(&self) -> &str;
9+
10+
/// Generates window configurations based on the available monitors.
11+
/// Returns a list of (UniqueName, WindowConfig, Monitor).
12+
fn generate_configs(&self, monitors: &[Monitor]) -> Vec<(String, WindowConf, Monitor)>;
13+
14+
/// Initializes a window instance for a specific monitor and configuration.
15+
/// This is where the Slint UI is created and event handlers are attached.
16+
/// The `unique_name` matches the one provided in `generate_configs`.
17+
fn create_instance(
18+
&self,
19+
unique_name: &str,
20+
monitor: &Monitor,
21+
) -> Result<Box<dyn PanelInstance>, Box<dyn Error>>;
22+
}

src/panel_manager/mod.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use hyprland::data::Monitor;
2+
use log::{debug, info};
3+
use spell_framework::{
4+
layer_properties::WindowConf, slint_adapter::SpellMultiWinHandler, wayland_adapter::SpellWin,
5+
};
6+
use std::error::Error;
7+
8+
pub mod factory;
9+
10+
pub use factory::PanelFactory;
11+
12+
/// Trait representing a tangible window instance (mostly for keeping the UI handle alive).
13+
pub trait PanelInstance {
14+
fn on_show(&self) {}
15+
fn on_hide(&self) {}
16+
}
17+
18+
/// The main manager that coordinates all windows.
19+
pub struct PanelManager {
20+
factories: Vec<Box<dyn PanelFactory>>,
21+
}
22+
23+
impl PanelManager {
24+
pub fn new() -> Self {
25+
Self {
26+
factories: Vec::new(),
27+
}
28+
}
29+
30+
pub fn register_factory<F: PanelFactory + 'static>(&mut self, factory: F) {
31+
self.factories.push(Box::new(factory));
32+
}
33+
34+
// Split the run to easier testing maybe? or just fix the ownership.
35+
}
36+
37+
/// Helper to run the main loop, we might need to expose a method that takes the instances
38+
/// and the windows and runs them.
39+
/// But `PanelManager` holds factories, it shouldn't hold the runtime state necessarily forever?
40+
/// Actually `run` should block.
41+
///
42+
/// Let's refine `run`.
43+
impl PanelManager {
44+
// ... (new, register_factory)
45+
46+
pub fn start(&self, monitors: &[Monitor]) -> Result<(), Box<dyn Error>> {
47+
// 1. Configuration Phase
48+
let mut factory_configs: Vec<(&Box<dyn PanelFactory>, Vec<(String, WindowConf, Monitor)>)> =
49+
Vec::new();
50+
51+
for factory in &self.factories {
52+
let configs = factory.generate_configs(monitors);
53+
if !configs.is_empty() {
54+
factory_configs.push((factory, configs));
55+
}
56+
}
57+
58+
let mut flat_configs: Vec<(&str, WindowConf)> = Vec::new();
59+
for (_, configs) in &factory_configs {
60+
for (name, conf, _) in configs {
61+
flat_configs.push((name.as_str(), conf.clone()));
62+
}
63+
}
64+
65+
// 2. Window Creation Phase
66+
let windows: Vec<SpellWin> = SpellMultiWinHandler::conjure_spells(flat_configs.clone());
67+
68+
// 3. UI Initialization Phase
69+
// We match them by order.
70+
let mut ui_instances: Vec<Box<dyn PanelInstance>> = Vec::new();
71+
72+
// We iterate through our factory config groupings, they should match the flattened list structure.
73+
for (factory, configs) in &factory_configs {
74+
for (name, _, monitor) in configs {
75+
let instance = factory.create_instance(name, monitor)?;
76+
ui_instances.push(instance);
77+
}
78+
}
79+
80+
// 4. Event Loop Phase
81+
let num_windows = windows.len();
82+
let states: Vec<_> = (0..num_windows).map(|_| None).collect();
83+
let callbacks: Vec<_> = (0..num_windows).map(|_| None).collect();
84+
85+
use spell_framework::enchant_spells;
86+
// imports needed
87+
88+
enchant_spells(windows, states, callbacks)?;
89+
90+
Ok(())
91+
}
92+
}

0 commit comments

Comments
 (0)