226 lines
7.9 KiB
Rust
226 lines
7.9 KiB
Rust
use cosmic::Element;
|
|
use cosmic::iced::{Length, window};
|
|
use cosmic::widget::{button, container, icon, mouse_area, row};
|
|
|
|
use crate::control::{ShellSnapshot, WindowSnapshot};
|
|
use crate::{FereseShell, Message, color, motion};
|
|
|
|
// Launch order stays stable as focus changes. Each open window gets its own
|
|
// entry, including multiple windows belonging to the same application.
|
|
fn windows(snapshot: &ShellSnapshot) -> Vec<&WindowSnapshot> {
|
|
let mut windows: Vec<_> = snapshot.windows.iter().collect();
|
|
windows.sort_by_key(|window| window.id);
|
|
windows
|
|
}
|
|
|
|
pub(super) fn caption(window: &WindowSnapshot) -> &str {
|
|
if !window.title.trim().is_empty() {
|
|
&window.title
|
|
} else if !window.app_id.is_empty() {
|
|
&window.app_id
|
|
} else {
|
|
ferese_config::tr!("Window")
|
|
}
|
|
}
|
|
|
|
impl FereseShell {
|
|
pub(super) fn view_window_tray(
|
|
&self,
|
|
bar: window::Id,
|
|
instance: &str,
|
|
allocated: Option<f32>,
|
|
height: f32,
|
|
) -> Element<'_, cosmic::Action<Message>> {
|
|
let windows = windows(&self.snapshot);
|
|
if windows.is_empty() {
|
|
return container(row([])).width(0).into();
|
|
}
|
|
let palette = self.config.theme.for_bar();
|
|
let foreground = color(palette.text_primary);
|
|
let mut entries = row([]).spacing(2);
|
|
let natural_width = windows.len() as f32 * 32. - 2.;
|
|
for window in windows {
|
|
let foreground = if window.urgent {
|
|
color(palette.accent)
|
|
} else {
|
|
foreground
|
|
};
|
|
let entry = button::custom(
|
|
container(icon(self.workspace_ui.app_icon(&window.app_id)).size(18))
|
|
.center_x(Length::Fill)
|
|
.center_y(Length::Fill),
|
|
)
|
|
.name(caption(window))
|
|
.width(30)
|
|
.height(height)
|
|
.padding(4)
|
|
.on_press(cosmic::Action::App(Message::ActivateWindow(window.id)));
|
|
let entry = motion::button(
|
|
self.render.with_radius_limit(5.),
|
|
entry,
|
|
foreground,
|
|
window.focused,
|
|
1.,
|
|
);
|
|
let target = cosmic::iced::advanced::widget::Id::new(format!(
|
|
"window-tray:{bar:?}:{instance}:{}",
|
|
window.id
|
|
));
|
|
let entry = mouse_area(container(entry).id(target.clone()))
|
|
.on_enter(cosmic::Action::App(Message::HoverWindow(
|
|
bar,
|
|
window.id,
|
|
target.clone(),
|
|
true,
|
|
)))
|
|
.on_exit(cosmic::Action::App(Message::HoverWindow(
|
|
bar,
|
|
window.id,
|
|
target.clone(),
|
|
false,
|
|
)));
|
|
let entry = if self.workspace_ui.hovering(bar, &target) {
|
|
entry
|
|
} else {
|
|
entry.on_move(move |_| {
|
|
cosmic::Action::App(Message::HoverWindow(bar, window.id, target.clone(), true))
|
|
})
|
|
};
|
|
entries = entries.push(entry);
|
|
}
|
|
cosmic::iced::widget::scrollable(entries)
|
|
.direction(cosmic::iced::widget::scrollable::Direction::Horizontal(
|
|
cosmic::iced::widget::scrollable::Scrollbar::default()
|
|
.width(0)
|
|
.scroller_width(0),
|
|
))
|
|
.width(allocated.unwrap_or(natural_width.min(360.)).max(0.))
|
|
.height(height)
|
|
.into()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn window(id: u64, title: &str) -> WindowSnapshot {
|
|
WindowSnapshot {
|
|
id,
|
|
title: title.into(),
|
|
app_id: "test.app".into(),
|
|
workspace: 1,
|
|
geometry: None,
|
|
focused: false,
|
|
urgent: false,
|
|
fullscreen: false,
|
|
maximized: false,
|
|
floating: false,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn each_icon_activates_its_own_window_at_the_drawn_bounds() {
|
|
use cosmic::iced::advanced::renderer::Headless;
|
|
use cosmic::iced::advanced::{Layout, Shell, clipboard, layout, widget};
|
|
use cosmic::iced::{Event, Rectangle, Size, mouse};
|
|
let renderer = cosmic::iced::futures::executor::block_on(cosmic::Renderer::new(
|
|
cosmic::iced::Font::default(),
|
|
cosmic::iced::Pixels(14.),
|
|
Some("tiny-skia"),
|
|
))
|
|
.unwrap();
|
|
let mut app = crate::tests::shell_with_measured_panel();
|
|
app.snapshot.windows = vec![window(1, "One"), window(2, "Two"), window(3, "Three")];
|
|
let bar = app.outputs[0].bar;
|
|
struct Bounds(Vec<(widget::Id, Option<Rectangle>)>);
|
|
impl widget::Operation for Bounds {
|
|
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
|
visit(self);
|
|
}
|
|
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
|
|
if let Some((_, found)) = self.0.iter_mut().find(|(target, _)| Some(&*target) == id)
|
|
{
|
|
*found = Some(bounds);
|
|
}
|
|
}
|
|
}
|
|
let mut bounds = Bounds(
|
|
(1..=3)
|
|
.map(|id| {
|
|
(
|
|
widget::Id::new(format!("window-tray:{bar:?}:test:{id}")),
|
|
None,
|
|
)
|
|
})
|
|
.collect(),
|
|
);
|
|
let mut view = app.view_window_tray(bar, "test", None, 28.);
|
|
let mut tree = widget::Tree::new(&view);
|
|
let node = view
|
|
.as_widget_mut()
|
|
.layout(
|
|
&mut tree,
|
|
&renderer,
|
|
&layout::Limits::new(Size::ZERO, Size::new(360., 36.)),
|
|
)
|
|
.move_to((300., 0.));
|
|
view.as_widget_mut()
|
|
.operate(&mut tree, Layout::new(&node), &renderer, &mut bounds);
|
|
for (index, (_, bounds)) in bounds.0.iter().enumerate() {
|
|
let point = bounds.expect("icon bounds").center();
|
|
let mut messages = Vec::new();
|
|
for event in [
|
|
mouse::Event::CursorMoved { position: point },
|
|
mouse::Event::ButtonPressed(mouse::Button::Left),
|
|
mouse::Event::ButtonReleased(mouse::Button::Left),
|
|
] {
|
|
view.as_widget_mut().update(
|
|
&mut tree,
|
|
&Event::Mouse(event),
|
|
Layout::new(&node),
|
|
mouse::Cursor::Available(point),
|
|
&renderer,
|
|
&mut clipboard::Null,
|
|
&mut Shell::new(&mut messages),
|
|
&node.bounds(),
|
|
);
|
|
}
|
|
let activated: Vec<_> = messages
|
|
.iter()
|
|
.filter_map(|m| match m {
|
|
cosmic::Action::App(Message::ActivateWindow(id)) => Some(*id),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(activated, [index as u64 + 1], "icon at {point:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn focus_changes_preserve_order_and_closed_windows_disappear() {
|
|
let mut snapshot = ShellSnapshot::default();
|
|
snapshot.windows = vec![window(3, "Third"), window(1, "First"), window(2, "Second")];
|
|
snapshot.windows[0].focused = true;
|
|
assert_eq!(
|
|
windows(&snapshot).iter().map(|w| w.id).collect::<Vec<_>>(),
|
|
[1, 2, 3]
|
|
);
|
|
snapshot.windows[0].focused = false;
|
|
snapshot.windows[1].focused = true;
|
|
snapshot.windows.retain(|w| w.id != 2);
|
|
assert_eq!(
|
|
windows(&snapshot).iter().map(|w| w.id).collect::<Vec<_>>(),
|
|
[1, 3]
|
|
);
|
|
assert_eq!(caption(windows(&snapshot)[0]), "First");
|
|
snapshot.windows.clear();
|
|
assert!(windows(&snapshot).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn empty_titles_fall_back_to_app_identity() {
|
|
assert_eq!(caption(&window(1, " ")), "test.app");
|
|
}
|
|
}
|