feat: frosted glass

This commit is contained in:
Ashley Wulber 2026-07-06 19:05:27 -04:00 • committed by Jacob Kauffmann
parent 42add3f075
commit 4bafd82500
4 changed files with 588 additions and 525 deletions

888
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -55,3 +55,11 @@ mock-backend = []
[profile.dev]
# Not usable at opt-level 0, at least with software renderer
opt-level = 1
# [patch.'https://github.com/pop-os/libcosmic']
# libcosmic = { path = "../libcosmic" }
# cosmic-config = { path = "../libcosmic/cosmic-config" }
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" }
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" }
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" }

View file

@ -11,20 +11,25 @@ use cctk::wayland_client::{Connection, Proxy};
use cctk::wayland_protocols::ext::workspace::v1::client::ext_workspace_handle_v1;
use clap::Parser;
use cosmic::app::{Application, CosmicFlags};
use cosmic::cctk::wayland_client::backend::ObjectId;
use cosmic::core::Auto;
use cosmic::iced::clipboard::dnd::{DndEvent, SourceEvent};
use cosmic::iced::event::wayland::{Event as WaylandEvent, LayerEvent, OutputEvent};
use cosmic::iced::keyboard::key::{Key, Named};
use cosmic::iced::mouse::ScrollDelta;
use cosmic::iced::platform_specific::shell::commands::layer_surface::{
destroy_layer_surface, get_layer_surface,
};
use cosmic::iced::platform_specific::shell::commands::layer_surface::destroy_layer_surface;
use cosmic::iced::runtime::platform_specific::wayland::CornerRadius;
use cosmic::iced::runtime::platform_specific::wayland::layer_surface::{
IcedOutput, SctkLayerSurfaceSettings,
IcedMargin, IcedOutput, SctkLayerSurfaceSettings,
};
use cosmic::iced::window::Id as SurfaceId;
use cosmic::iced::{self, Size, Subscription, Task};
use cosmic::iced::window::{self, Id as SurfaceId};
use cosmic::iced::{self, Rectangle, Size, Subscription, Task};
use cosmic::scroll::DiscreteScrollState;
use cosmic::{cctk, dbus_activation};
use cosmic::surface::action::LiveSettings;
use cosmic::widget::rectangle_tracker::{
RectangleTracker, RectangleUpdate, rectangle_tracker_subscription,
};
use cosmic::{cctk, dbus_activation, widget};
use cosmic_comp_config::CosmicCompConfig;
use cosmic_config::CosmicConfigEntry;
use cosmic_config::cosmic_config_derive::CosmicConfigEntry;
@ -114,6 +119,7 @@ enum Msg {
PanelConfig(CosmicPanelConfig),
ActionOnTyping(String),
Ignore,
Rectangle(RectangleUpdate<RectId>),
}
#[derive(Clone, Debug)]
@ -174,6 +180,13 @@ struct Conf {
bg: cosmic_bg_config::state::State,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RectId {
id: window::Id,
toplevel_id: Option<ObjectId>,
widget_id: Option<widget::Id>,
}
#[derive(Default)]
struct App {
capture_filter: backend::CaptureFilter,
@ -194,6 +207,8 @@ struct App {
dbus_interface: Option<dbus::Interface>,
panel_configs: HashMap<String, Option<CosmicPanelConfig>>,
action_on_typing_activated: bool,
rectangle_tracker: Option<RectangleTracker<RectId>>,
rects: HashMap<RectId, Rectangle>,
}
#[derive(Debug, Default)]
@ -234,16 +249,24 @@ impl App {
output: output.clone(),
},
);
get_layer_surface(SctkLayerSurfaceSettings {
id,
keyboard_interactivity: KeyboardInteractivity::Exclusive,
namespace: "cosmic-workspace-overview".into(),
layer: Layer::Top,
size: Some((None, None)),
output: IcedOutput::Output(output),
anchor: Anchor::all(),
..Default::default()
})
cosmic::surface::surface_task::<Msg>(cosmic::surface::action::simple_layer_shell::<Msg>(
|| LiveSettings {
padding: Some(IcedMargin::default()),
corners: Some(CornerRadius::default()),
blur: Some(false),
},
move || SctkLayerSurfaceSettings {
id,
keyboard_interactivity: KeyboardInteractivity::Exclusive,
namespace: "cosmic-workspace-overview".into(),
layer: Layer::Top,
size: Some((None, None)),
output: IcedOutput::Output(output.clone()),
anchor: Anchor::all(),
..Default::default()
},
None::<fn() -> cosmic::Element<'static, cosmic::Action<Msg>>>,
))
}
fn destroy_surface(&mut self, output: &wl_output::WlOutput) -> Task<cosmic::Action<Msg>> {
@ -429,7 +452,9 @@ impl Application for App {
type Flags = Args;
const APP_ID: &'static str = "com.system76.CosmicWorkspaces";
fn init(core: cosmic::app::Core, _flags: Self::Flags) -> (Self, Task<cosmic::Action<Msg>>) {
fn init(mut core: cosmic::app::Core, _flags: Self::Flags) -> (Self, Task<cosmic::Action<Msg>>) {
core.set_app_type(cosmic::core::AppType::System);
core.set_auto_blur(Auto::Popup | Auto::Window);
(
Self {
core,
@ -443,6 +468,45 @@ impl Application for App {
fn update(&mut self, message: Msg) -> Task<cosmic::Action<Msg>> {
match message {
Msg::Rectangle(u) => match u {
RectangleUpdate::Rectangle(r) => {
// TODO should we reuse previously tracked rectangles on surface creation
self.rects.insert(r.0.clone(), r.1);
let active = cosmic::theme::active();
let rad = active
.cosmic()
.radius_s()
.map(|x| if x < 4.0 { x } else { x + 8.0 });
let strips = self
.rects
.iter()
.filter(|(id, _)| r.0.id == id.id)
.map(|(id, r)| {
let rad = if id.toplevel_id.is_none() {
CornerRadius {
top_left: rad[0] as u32,
top_right: rad[1] as u32,
bottom_left: rad[3] as u32,
bottom_right: rad[2] as u32,
}
} else {
CornerRadius::default()
};
cosmic::surface::corner_radius::rounded_rect_strips(*r, rad)
})
.flatten();
return cosmic::iced::platform_specific::shell::commands::blur::blur(
r.0.id,
Some(strips.collect()),
)
.discard();
}
RectangleUpdate::Init(tracker) => {
self.rectangle_tracker = Some(tracker);
}
},
Msg::SourceFinished => {
self.drag_surface = None;
}
@ -879,6 +943,7 @@ impl Application for App {
config_subscription,
comp_config_subscription,
bg_subscription,
rectangle_tracker_subscription(0).map(|update| Msg::Rectangle(update.1)),
];
if let Some(conn) = self.conn.clone() {
subscriptions.push(backend::subscription(conn).map(Msg::Wayland));
@ -895,8 +960,12 @@ impl Application for App {
}
fn view_window(&self, id: iced::window::Id) -> cosmic::Element<'_, Self::Message> {
if let Some(surface) = self.layer_surfaces.get(&id) {
return view::layer_surface(self, surface);
if let Some((surface, rectangle_track)) = self
.layer_surfaces
.get(&id)
.zip(self.rectangle_tracker.as_ref())
{
return view::layer_surface(self, surface, id, rectangle_track);
}
log::error!("non-existant layer shell id {}?", id);
cosmic::widget::text("workspaces").into()

View file

@ -1,22 +1,23 @@
use cosmic::Apply;
use cosmic::cctk::cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1;
use cosmic::cctk::cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2;
use cosmic::cctk::wayland_client::Proxy;
use cosmic::cctk::wayland_client::protocol::wl_output;
use cosmic::cctk::wayland_protocols::ext::workspace::v1::client::ext_workspace_handle_v1;
use cosmic::iced::advanced::layout::flex::Axis;
use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
use cosmic::iced::core::Shadow;
use cosmic::iced::core::text::{Ellipsize, EllipsizeHeightLimit};
use cosmic::iced::core::{Shadow, window};
use cosmic::iced::platform_specific::shell::subsurface_widget::Subsurface;
use cosmic::iced::widget::{column, row};
use cosmic::iced::{self, Alignment, Border, Length};
use cosmic::widget::{self, Widget};
use cosmic::widget::{self, Widget, rectangle_tracker};
use cosmic_comp_config::workspace::WorkspaceLayout;
use std::collections::HashSet;
use crate::backend::{self, CaptureImage};
use crate::dnd::{Drag, DragSurface, DragToplevel, DragWorkspace, DropTarget};
use crate::{App, LayerSurface, Msg, Toplevel, Workspace};
use crate::{App, LayerSurface, Msg, RectId, Toplevel, Workspace};
fn dnd_source_with_drag_surface<D: AsMimeTypes + Send + Clone + 'static>(
drag_content: D,
@ -69,6 +70,8 @@ where
pub(crate) fn layer_surface<'a>(
app: &'a App,
surface: &'a LayerSurface,
window_id: window::Id,
rectangle_track: &rectangle_tracker::RectangleTracker<RectId>,
) -> cosmic::Element<'a, Msg> {
let mut drag_toplevel = None;
let mut drag_workspace = None;
@ -89,6 +92,7 @@ pub(crate) fn layer_surface<'a>(
.flat_map(|t| &t.info.workspace)
.collect::<HashSet<_>>();
let layout = app.conf.workspace_config.workspace_layout;
// track this rectangle
let sidebar = workspaces_sidebar(
app.workspaces.for_output(&surface.output),
&workspaces_with_toplevels,
@ -96,6 +100,8 @@ pub(crate) fn layer_surface<'a>(
layout,
app.drop_target.as_ref(),
drag_workspace,
window_id,
rectangle_track,
);
let toplevels = toplevel_previews(
app.toplevels.0.iter().filter(|i| {
@ -111,6 +117,8 @@ pub(crate) fn layer_surface<'a>(
}),
layout,
drag_toplevel,
window_id,
rectangle_track,
);
// TODO multiple active workspaces? Not currently supported by cosmic.
let first_active_workspace = app
@ -146,6 +154,7 @@ pub(crate) fn layer_surface<'a>(
let container = widget::container(container).padding(panel_regions);
let output = surface.output.clone();
widget::mouse_area(container)
.on_scroll(move |delta| Msg::OnScroll(output.clone(), delta))
.into()
@ -162,12 +171,12 @@ fn pin_button_style(theme: &cosmic::Theme, is_pinned: bool) -> cosmic::widget::b
let bg_color = if is_pinned {
theme.cosmic().accent.base.into()
} else {
theme.cosmic().primary.base.into()
theme.cosmic().primary(theme.transparent).base.into()
};
let icon_color = if is_pinned {
theme.cosmic().accent.on.into()
} else {
theme.cosmic().primary.on.into()
theme.cosmic().primary(theme.transparent).on.into()
};
cosmic::widget::button::Style {
icon_color: Some(icon_color),
@ -322,6 +331,7 @@ fn workspace_item(
{
button = button.on_press(Msg::ActivateWorkspace(workspace.handle().clone()));
}
button.into()
}
@ -420,6 +430,8 @@ fn workspaces_sidebar<'a>(
layout: WorkspaceLayout,
drop_target: Option<&DropTarget>,
drag_workspace: Option<&'a backend::ExtWorkspaceHandleV1>,
window_id: window::Id,
rectangle_track: &rectangle_tracker::RectangleTracker<RectId>,
) -> cosmic::Element<'a, Msg> {
let mut sidebar_entries = Vec::new();
for workspace in workspaces {
@ -480,26 +492,36 @@ fn workspaces_sidebar<'a>(
widget::container(crate::widgets::workspace_bar(sidebar_entries, axis)).padding(8.0);
widget::container(
widget::container(sidebar_entries_container)
.width(width)
.height(height)
.class(cosmic::theme::Container::custom(|theme| {
cosmic::iced::widget::container::Style {
text_color: Some(theme.cosmic().on_bg_color().into()),
icon_color: Some(theme.cosmic().on_bg_color().into()),
background: Some(iced::Color::from(theme.cosmic().background.base).into()),
border: Border {
radius: theme
.cosmic()
.radius_s()
.map(|x| if x < 4.0 { x } else { x + 8.0 })
.into(),
..Default::default()
},
shadow: Shadow::default(),
snap: true,
}
})),
rectangle_track.container(
RectId {
id: window_id,
toplevel_id: None,
widget_id: None,
},
widget::container(sidebar_entries_container)
.width(width)
.height(height)
.class(cosmic::theme::Container::custom(|theme| {
cosmic::iced::widget::container::Style {
text_color: Some(theme.cosmic().on_bg_color().into()),
icon_color: Some(theme.cosmic().on_bg_color().into()),
background: Some(
iced::Color::from(theme.cosmic().background(theme.transparent).base)
.into(),
),
border: Border {
radius: theme
.cosmic()
.radius_s()
.map(|x| if x < 4.0 { x } else { x + 8.0 })
.into(),
..Default::default()
},
shadow: Shadow::default(),
snap: true,
}
})),
),
)
.padding(8)
.into()
@ -524,6 +546,8 @@ fn toplevel_preview(toplevel: &Toplevel, is_being_dragged: bool) -> cosmic::Elem
.align_y(Alignment::Center);
let title = row![
// TODO tracker for each of these buttons?
// So that they can be blurred individually like the sidebar?
widget::button::custom(label)
.on_press(Msg::ActivateToplevel(toplevel.handle.clone()))
.class(cosmic::theme::Button::Icon)
@ -532,7 +556,7 @@ fn toplevel_preview(toplevel: &Toplevel, is_being_dragged: bool) -> cosmic::Elem
.class(cosmic::theme::Container::custom(|theme| {
cosmic::iced::widget::container::Style {
background: Some(
iced::Color::from(theme.cosmic().background.component.base).into(),
iced::Color::from(theme.cosmic().background(false).component.base).into(),
),
border: Border {
color: theme.cosmic().bg_divider().into(),
@ -570,15 +594,24 @@ fn toplevel_preview(toplevel: &Toplevel, is_being_dragged: bool) -> cosmic::Elem
.into()
}
fn toplevel_previews_entry(
toplevel: &Toplevel,
fn toplevel_previews_entry<'a>(
toplevel: &'a Toplevel,
is_being_dragged: bool,
) -> cosmic::Element<'_, Msg> {
window_id: window::Id,
rectangle_track: &rectangle_tracker::RectangleTracker<RectId>,
) -> cosmic::Element<'a, Msg> {
// Dragged window still takes up space until moved, but isn't rendered while drag surface is
// shown.
let preview = crate::widgets::visibility_wrapper(
toplevel_preview(toplevel, is_being_dragged),
!is_being_dragged,
let preview = rectangle_track.container(
RectId {
id: window_id,
toplevel_id: Some(toplevel.handle.id()),
widget_id: None,
},
crate::widgets::visibility_wrapper(
toplevel_preview(toplevel, is_being_dragged),
!is_being_dragged,
),
);
let toplevel2 = toplevel.clone();
dnd_source_with_drag_surface(
@ -594,13 +627,22 @@ fn toplevel_previews<'a>(
toplevels: impl Iterator<Item = &'a Toplevel>,
layout: WorkspaceLayout,
drag_toplevel: Option<&'a backend::ExtForeignToplevelHandleV1>,
window_id: window::Id,
rectangle_track: &rectangle_tracker::RectangleTracker<RectId>,
) -> cosmic::Element<'a, Msg> {
let (width, height) = match layout {
WorkspaceLayout::Vertical => (Length::FillPortion(4), Length::Fill),
WorkspaceLayout::Horizontal => (Length::Fill, Length::FillPortion(4)),
};
let entries = toplevels
.map(|t| toplevel_previews_entry(t, drag_toplevel == Some(&t.handle)))
.map(|t| {
toplevel_previews_entry(
t,
drag_toplevel == Some(&t.handle),
window_id,
rectangle_track,
)
})
.collect();
//row(entries)
widget::mouse_area(