Match shell squircle outlines with compositor masks
Use shared corner geometry for shell controls, window clipping, materials and blur masks. Preserve circular pills and controls where appropriate, track contour changes in damage, and use tagged ferese-shape and libcosmic dependencies.
This commit is contained in:
1 parent
03a265b97f
commit
d75c93db85
31 files changed
+453
-80
No files matched your search
@@ -457,9 +457,11 @@ impl Locker {
|
||||
text_color: Some(on),
|
||||
icon_color: Some(on),
|
||||
border: iced::Border {
|
||||
shape: cosmic::iced::border::Shape::Continuous,
|
||||
radius: (avatar_size / 2.).into(),
|
||||
width: 1.,
|
||||
color: a.accent.scale_alpha(0.3),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
@@ -1130,6 +1130,120 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "release scroll/render sample; requires both renderer backends"]
|
||||
fn profile_dense_pages_on_both_renderers() {
|
||||
use cosmic::iced::advanced::renderer::{Headless, Renderer as _};
|
||||
use cosmic::iced::advanced::{Layout, Shell, clipboard, layout, mouse, renderer, widget::Tree};
|
||||
use cosmic::iced::{Color, Event, Font, Pixels, Point, Rectangle, Size};
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let bounds = Rectangle::with_size(Size::new(1000.0, 780.0));
|
||||
let scale = 1.25;
|
||||
let size = Size::new(1250, 975);
|
||||
|
||||
for backend in ["wgpu", "tiny-skia"] {
|
||||
let mut renderer = runtime
|
||||
.block_on(<cosmic::Renderer as Headless>::new(
|
||||
Font::default(),
|
||||
Pixels(14.0),
|
||||
Some(backend),
|
||||
))
|
||||
.expect("requested renderer");
|
||||
eprintln!("renderer={}", Headless::name(&renderer));
|
||||
|
||||
for page in [Page::Shortcuts, Page::Appearance] {
|
||||
let mut app = shortcuts(500);
|
||||
app.page = page;
|
||||
let theme = app.native_palette.native_theme();
|
||||
let mut tree: Option<Tree> = None;
|
||||
let mut samples = Vec::new();
|
||||
|
||||
for frame in 0..48 {
|
||||
let mut messages = Vec::new();
|
||||
let mut clipboard = clipboard::Null;
|
||||
let mut shell = Shell::new(&mut messages);
|
||||
let cursor = mouse::Cursor::Available(Point::new(700.0, 450.0));
|
||||
let started = std::time::Instant::now();
|
||||
let mut view = app.page_view();
|
||||
let tree = tree.get_or_insert_with(|| Tree::new(view.as_widget()));
|
||||
tree.diff(view.as_widget_mut());
|
||||
let node =
|
||||
view.as_widget_mut()
|
||||
.layout(tree, &renderer, &layout::Limits::new(Size::ZERO, bounds.size()));
|
||||
view.as_widget_mut().update(
|
||||
tree,
|
||||
&Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines {
|
||||
x: 0.0,
|
||||
y: if frame < 24 { -1.0 } else { 1.0 },
|
||||
},
|
||||
}),
|
||||
Layout::new(&node),
|
||||
cursor,
|
||||
&renderer,
|
||||
&mut clipboard,
|
||||
&mut shell,
|
||||
&bounds,
|
||||
);
|
||||
let layout_elapsed = started.elapsed();
|
||||
renderer.reset(bounds);
|
||||
let draw_started = std::time::Instant::now();
|
||||
view.as_widget().draw(
|
||||
tree,
|
||||
&mut renderer,
|
||||
&theme,
|
||||
&renderer::Style {
|
||||
text_color: Color::WHITE,
|
||||
icon_color: Color::WHITE,
|
||||
scale_factor: scale as f64,
|
||||
},
|
||||
Layout::new(&node),
|
||||
cursor,
|
||||
&bounds,
|
||||
);
|
||||
let draw_elapsed = draw_started.elapsed();
|
||||
drop(view);
|
||||
let render_started = std::time::Instant::now();
|
||||
let pixels = Headless::screenshot(&mut renderer, size, scale, theme.cosmic().bg_color().into());
|
||||
std::hint::black_box(pixels);
|
||||
let render_elapsed = render_started.elapsed();
|
||||
|
||||
for message in messages {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
|
||||
if frame >= 8 {
|
||||
samples.push((
|
||||
layout_elapsed.as_micros(),
|
||||
draw_elapsed.as_micros(),
|
||||
render_elapsed.as_micros(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for (stage, index) in [("layout", 0), ("draw_queue", 1), ("render_and_readback", 2)] {
|
||||
let mut times = samples
|
||||
.iter()
|
||||
.map(|sample| match index {
|
||||
0 => sample.0,
|
||||
1 => sample.1,
|
||||
_ => sample.2,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
times.sort_unstable();
|
||||
eprintln!(
|
||||
"backend={backend} page={page:?} stage={stage} median_us={} p95_us={}",
|
||||
times[times.len() / 2],
|
||||
times[times.len() * 95 / 100]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabling_auto_keeps_the_effective_appearance() {
|
||||
for appearance in [
|
||||
|
||||
@@ -17,7 +17,7 @@ libc.workspace = true
|
||||
# Keep GPU rendering available as a fallback/override. The shell defaults to
|
||||
# tiny-skia to avoid a second GPU initialization during desktop startup.
|
||||
libcosmic = { workspace = true, features = ["wgpu", "a11y"] }
|
||||
iced_accessibility = { git = "https://github.com/pop-os/libcosmic", rev = "03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" }
|
||||
iced_accessibility = { git = "https://github.com/ferese-wm/libcosmic", tag = "ferese-v0.1.0" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::{
|
||||
Background, BarMetrics, Border, Color, Element, FereseShell, Length, Message, ShellSnapshot, ShellTheme, alignment,
|
||||
bar_icon, button, color, color_with_opacity, container, control, motion, row, status_ui, text, theme, window,
|
||||
};
|
||||
use cosmic::iced::border::Shape as BorderShape;
|
||||
|
||||
impl FereseShell {
|
||||
pub(super) fn output_for_bar(&self, id: window::Id) -> Option<&control::OutputSnapshot> {
|
||||
@@ -327,9 +328,11 @@ pub(super) fn workspace_selector_style(
|
||||
None
|
||||
},
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
color: color_with_opacity(shell_theme.accent, 0.55),
|
||||
width: if active_elsewhere { 1.0 } else { 0.0 },
|
||||
radius: shell_theme.material_radius.min(14.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
@@ -350,9 +353,11 @@ pub(super) fn bar_group_style(theme: ShellTheme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(color_with_opacity(theme.border, 0.25))),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
color: color(theme.border),
|
||||
width: 1.0,
|
||||
radius: theme.material_radius.min(16.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::{
|
||||
configured_font, container, destroy_layer_surface, format_bar_time, motion, note_store, row, set_anchor,
|
||||
set_margin, stacked_clock_digits, text, theme, window,
|
||||
};
|
||||
use cosmic::iced::border::Shape as BorderShape;
|
||||
|
||||
impl FereseShell {
|
||||
pub(super) fn flush_note_changes(&mut self) -> Task<Message> {
|
||||
@@ -398,6 +399,7 @@ impl FereseShell {
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background,
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -613,6 +615,7 @@ impl FereseShell {
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background,
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use cosmic::iced::border::Shape as BorderShape;
|
||||
use cosmic::iced::widget::scrollable::{Direction, Scrollbar};
|
||||
use cosmic::widget::{column, scrollable};
|
||||
|
||||
@@ -56,6 +57,23 @@ fn card_button<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
fn card_surface<'a>(
|
||||
content: impl Into<Element<'a, cosmic::Action<Message>>>,
|
||||
history: bool,
|
||||
style: container::Style,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
container(content)
|
||||
.id(if history {
|
||||
"ferese-history-card"
|
||||
} else {
|
||||
"ferese-blur-card"
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.clip_to_border(true)
|
||||
.class(theme::Container::custom(move |_| style))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn icon_control<'a>(
|
||||
source: &'static [u8],
|
||||
label: &'static str,
|
||||
@@ -82,6 +100,7 @@ fn icon_control<'a>(
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(background)),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: motion::radius(12.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -287,6 +306,7 @@ impl FereseShell {
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(well)),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: motion::radius(15.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -313,6 +333,7 @@ impl FereseShell {
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(badge_fill.scale_alpha(opacity))),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: motion::radius(8.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -448,15 +469,10 @@ impl FereseShell {
|
||||
.push(actions);
|
||||
}
|
||||
}
|
||||
let card = container(face)
|
||||
.id(if history {
|
||||
"ferese-history-card"
|
||||
} else {
|
||||
"ferese-blur-card"
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.clip(true)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
let card = card_surface(
|
||||
face,
|
||||
history,
|
||||
container::Style {
|
||||
background: if history {
|
||||
Some(Background::Color(Color {
|
||||
a: 0.045 * opacity,
|
||||
@@ -466,14 +482,16 @@ impl FereseShell {
|
||||
(!compositor_material).then_some(Background::Color(base))
|
||||
},
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
||||
width: if history { 0.0 } else { 1.0 },
|
||||
radius: palette.material_radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
snap: true,
|
||||
..Default::default()
|
||||
}));
|
||||
let card: Element<'_, cosmic::Action<Message>> = card.into();
|
||||
},
|
||||
);
|
||||
let card: Element<'_, cosmic::Action<Message>> = if count > 1 {
|
||||
// With translucent materials, complete backing rectangles show
|
||||
// through the face and multiply its tint. Paint only the exposed
|
||||
@@ -488,6 +506,7 @@ impl FereseShell {
|
||||
..foreground
|
||||
})),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: [0.0, 0.0, motion::radius(6.0), motion::radius(6.0)].into(),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -648,6 +667,7 @@ impl FereseShell {
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(base)),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: palette.material_radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -665,6 +685,7 @@ impl FereseShell {
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(well)),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: motion::radius(28.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -766,9 +787,11 @@ impl FereseShell {
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: (!compositor_material).then_some(Background::Color(color(palette.surface_popover))),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
||||
width: 1.0,
|
||||
radius: palette.material_radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
snap: true,
|
||||
..Default::default()
|
||||
@@ -803,3 +826,91 @@ impl FereseShell {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmic::iced::advanced::renderer::{Headless, Renderer as _};
|
||||
use cosmic::iced::advanced::{Layout, layout, mouse, widget::Tree};
|
||||
use cosmic::iced::{Font, Pixels, Rectangle, Size};
|
||||
|
||||
#[test]
|
||||
fn notification_action_hover_stays_inside_card_corners() {
|
||||
check_action_hover("tiny-skia");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a GPU or software Vulkan adapter"]
|
||||
fn gpu_notification_action_hover_stays_inside_card_corners() {
|
||||
check_action_hover("wgpu");
|
||||
}
|
||||
|
||||
fn check_action_hover(backend: &str) {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut renderer = runtime
|
||||
.block_on(<cosmic::Renderer as Headless>::new(
|
||||
Font::default(),
|
||||
Pixels(14.0),
|
||||
Some(backend),
|
||||
))
|
||||
.unwrap();
|
||||
let theme = cosmic::Theme::dark();
|
||||
let bounds = Rectangle::with_size(Size::new(368.0, 114.0));
|
||||
for history in [false, true] {
|
||||
for scale in [1.0, 1.25, 1.5] {
|
||||
let action = card_button(
|
||||
cosmic::widget::Space::new().height(38),
|
||||
Message::InvokeNotification(1, "action".into()),
|
||||
Color::WHITE,
|
||||
Color::WHITE,
|
||||
false,
|
||||
"Action".into(),
|
||||
);
|
||||
let face = column([cosmic::widget::Space::new().height(76).into(), action]);
|
||||
let mut card = card_surface(
|
||||
face,
|
||||
history,
|
||||
container::Style {
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: 14.0.into(),
|
||||
width: if history { 0.0 } else { 1.0 },
|
||||
..Default::default()
|
||||
},
|
||||
snap: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let mut tree = Tree::new(card.as_widget());
|
||||
let node =
|
||||
card.as_widget_mut()
|
||||
.layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, bounds.size()));
|
||||
renderer.reset(bounds);
|
||||
card.as_widget().draw(
|
||||
&tree,
|
||||
&mut renderer,
|
||||
&theme,
|
||||
&Default::default(),
|
||||
Layout::new(&node),
|
||||
mouse::Cursor::Available((80.0, 100.0).into()),
|
||||
&bounds,
|
||||
);
|
||||
let size = Size::new((bounds.width * scale) as u32, (bounds.height * scale) as u32);
|
||||
let pixels = Headless::screenshot(&mut renderer, size, scale, Color::TRANSPARENT);
|
||||
let alpha = |x: u32, y: u32| pixels[((y * size.width + x) * 4 + 3) as usize];
|
||||
assert_eq!(
|
||||
alpha(0, size.height - 1),
|
||||
0,
|
||||
"{backend}, scale={scale}, history={history}"
|
||||
);
|
||||
assert!(
|
||||
alpha(size.width / 2, size.height - 4) > 200,
|
||||
"the action hover must still render"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use cosmic::iced::border::Shape as BorderShape;
|
||||
use cosmic::iced::{Alignment, Background, Border, Rectangle, alignment};
|
||||
use cosmic::widget::{button, container, row};
|
||||
use cosmic::{Element, theme};
|
||||
@@ -82,6 +83,7 @@ impl FereseShell {
|
||||
move |_| container::Style {
|
||||
background: Some(Background::Color(foreground)),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: motion::radius(2.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use cosmic::iced::border::Shape as BorderShape;
|
||||
use cosmic::iced::{Alignment, Background, Border, Color, Length};
|
||||
use cosmic::widget::{button, column, container, row, slider};
|
||||
use cosmic::{Element, theme};
|
||||
@@ -32,6 +33,7 @@ pub(super) fn status_summary<'a>(
|
||||
..badge_color
|
||||
})),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: motion::radius(16.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -79,6 +81,7 @@ pub(super) fn level_meter(value: u8, foreground: Color, opacity: f32) -> Element
|
||||
}
|
||||
})),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
radius: motion::radius(2.5).into(),
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ mod system;
|
||||
|
||||
use bar::status_icon;
|
||||
use controls::{control_card, menu_button, toggle_row};
|
||||
use cosmic::iced::border::Shape as BorderShape;
|
||||
use cosmic::widget::column;
|
||||
use status::{Action, Snapshot};
|
||||
|
||||
@@ -242,9 +243,11 @@ impl FereseShell {
|
||||
text_color: Some(primary),
|
||||
icon_color: Some(primary),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
color: color_with_opacity(theme.border, p),
|
||||
width: 1.0,
|
||||
radius: theme.material_radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
snap: true,
|
||||
..Default::default()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use cosmic::iced::border::Shape as BorderShape;
|
||||
use cosmic::iced::widget::Space;
|
||||
use cosmic::widget::column;
|
||||
|
||||
@@ -620,9 +621,11 @@ impl FereseShell {
|
||||
text_color: Some(palette.text),
|
||||
icon_color: Some(palette.text),
|
||||
border: Border {
|
||||
shape: BorderShape::Continuous,
|
||||
color: palette.muted.scale_alpha(0.16),
|
||||
width: 1.0,
|
||||
radius: theme.material_radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
snap: true,
|
||||
..Default::default()
|
||||
|
||||
Reference in new issue
Block a user