Fix panel review findings and polkit library lifetime
This commit is contained in:
1 parent
f7e5efb245
commit
305f49a62f
13 files changed
+436
-676
No files matched your search
@@ -1,60 +1,9 @@
|
||||
use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, renderer, widget};
|
||||
use cosmic::iced::{Event, Length, Rectangle, Size, Vector};
|
||||
use cosmic::{Element, Theme};
|
||||
use ferese_panel::PanelSurface;
|
||||
|
||||
pub(crate) fn island_id() -> widget::Id {
|
||||
static ID: std::sync::OnceLock<widget::Id> = std::sync::OnceLock::new();
|
||||
ID.get_or_init(widget::Id::unique).clone()
|
||||
}
|
||||
|
||||
pub(crate) fn input_id() -> widget::Id {
|
||||
static ID: std::sync::OnceLock<widget::Id> = std::sync::OnceLock::new();
|
||||
ID.get_or_init(widget::Id::unique).clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn frame<'a, M: 'a>(
|
||||
content: Element<'a, M>,
|
||||
mode: PanelSurface,
|
||||
radius: f32,
|
||||
materials: Vec<(widget::Id, f32)>,
|
||||
hover: Option<HoverTarget<'a, M>>,
|
||||
present: impl Fn(&[[f32; 5]], &[f32]) + 'a,
|
||||
changed: impl Fn(Vec<[f32; 5]>) -> M + 'a,
|
||||
) -> Element<'a, M> {
|
||||
Element::new(Presentation {
|
||||
content,
|
||||
mode,
|
||||
radius,
|
||||
materials,
|
||||
hover,
|
||||
present: Box::new(present),
|
||||
changed: Some(Box::new(changed)),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn hover<'a, M: 'a>(content: Element<'a, M>, hover: Option<HoverTarget<'a, M>>) -> Element<'a, M> {
|
||||
Element::new(Presentation {
|
||||
content,
|
||||
mode: PanelSurface::None,
|
||||
radius: 0.,
|
||||
materials: Vec::new(),
|
||||
hover,
|
||||
present: Box::new(|_, _| {}),
|
||||
changed: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
mode: Option<PanelSurface>,
|
||||
current: Vec<[f32; 5]>,
|
||||
scratch: Vec<[f32; 5]>,
|
||||
input: Vec<[f32; 5]>,
|
||||
input_scratch: Vec<[f32; 5]>,
|
||||
scroll_starts: Vec<(usize, usize)>,
|
||||
opacity_scratch: Vec<f32>,
|
||||
Element::new(Hover { content, hover })
|
||||
}
|
||||
|
||||
pub(super) struct HoverTarget<'a, M> {
|
||||
@@ -79,29 +28,12 @@ impl widget::Operation for HoverBounds<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
type Changed<'a, M> = Box<dyn Fn(Vec<[f32; 5]>) -> M + 'a>;
|
||||
|
||||
type Present<'a> = Box<dyn Fn(&[[f32; 5]], &[f32]) + 'a>;
|
||||
|
||||
struct Presentation<'a, M> {
|
||||
struct Hover<'a, M> {
|
||||
content: Element<'a, M>,
|
||||
mode: PanelSurface,
|
||||
radius: f32,
|
||||
materials: Vec<(widget::Id, f32)>,
|
||||
hover: Option<HoverTarget<'a, M>>,
|
||||
present: Present<'a>,
|
||||
changed: Option<Changed<'a, M>>,
|
||||
}
|
||||
|
||||
impl<M> Widget<M, Theme, cosmic::Renderer> for Presentation<'_, M> {
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
widget::tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> widget::tree::State {
|
||||
widget::tree::State::new(State::default())
|
||||
}
|
||||
|
||||
impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
vec![widget::Tree::new(&self.content)]
|
||||
}
|
||||
@@ -181,54 +113,6 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Presentation<'_, M> {
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
if self.changed.is_none() || !matches!(event, Event::Window(cosmic::iced::window::Event::RedrawRequested(_))) {
|
||||
return;
|
||||
}
|
||||
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let mut collector = Collect {
|
||||
regions: std::mem::take(&mut state.scratch),
|
||||
input: std::mem::take(&mut state.input_scratch),
|
||||
scroll_starts: std::mem::take(&mut state.scroll_starts),
|
||||
radius: self.radius,
|
||||
materials: &self.materials,
|
||||
opacities: std::mem::take(&mut state.opacity_scratch),
|
||||
};
|
||||
collector.regions.clear();
|
||||
collector.opacities.clear();
|
||||
collector.input.clear();
|
||||
collector.scroll_starts.clear();
|
||||
if self.mode == PanelSurface::None {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(&mut tree.children[0], layout, renderer, &mut collector);
|
||||
} else {
|
||||
let bounds = layout.bounds();
|
||||
collector
|
||||
.regions
|
||||
.push([bounds.x, bounds.y, bounds.width, bounds.height, self.radius]);
|
||||
collector.opacities.push(1.);
|
||||
}
|
||||
|
||||
collector.clip_from(0, layout.bounds(), Vector::ZERO);
|
||||
collector.clip_input_from(0, layout.bounds(), Vector::ZERO);
|
||||
collector.discard_empty();
|
||||
(self.present)(&collector.regions, &collector.opacities);
|
||||
if state.mode != Some(self.mode) || state.current != collector.regions || state.input != collector.input {
|
||||
state.mode = Some(self.mode);
|
||||
state.current.clone_from(&collector.regions);
|
||||
state.input.clone_from(&collector.input);
|
||||
shell.publish((self.changed.as_ref().unwrap())(if self.mode == PanelSurface::Solid {
|
||||
collector.regions.clone()
|
||||
} else {
|
||||
collector.input.clone()
|
||||
}));
|
||||
}
|
||||
|
||||
state.opacity_scratch = collector.opacities;
|
||||
state.scratch = collector.regions;
|
||||
state.input_scratch = collector.input;
|
||||
state.scroll_starts = collector.scroll_starts;
|
||||
}
|
||||
|
||||
fn draw(
|
||||
@@ -284,350 +168,3 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Presentation<'_, M> {
|
||||
.overlay(&mut tree.children[0], layout, renderer, viewport, translation)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Collect<'a> {
|
||||
regions: Vec<[f32; 5]>,
|
||||
input: Vec<[f32; 5]>,
|
||||
scroll_starts: Vec<(usize, usize)>,
|
||||
radius: f32,
|
||||
materials: &'a [(widget::Id, f32)],
|
||||
opacities: Vec<f32>,
|
||||
}
|
||||
|
||||
impl Collect<'_> {
|
||||
fn discard_empty(&mut self) {
|
||||
let mut index = 0;
|
||||
self.opacities.retain(|_| {
|
||||
let keep = self.regions[index][2] > 0. && self.regions[index][3] > 0.;
|
||||
index += 1;
|
||||
keep
|
||||
});
|
||||
self.regions.retain(|region| region[2] > 0. && region[3] > 0.);
|
||||
self.input.retain(|region| region[2] > 0. && region[3] > 0.);
|
||||
}
|
||||
|
||||
fn clip_input_from(&mut self, start: usize, viewport: Rectangle, translation: Vector) {
|
||||
std::mem::swap(&mut self.regions, &mut self.input);
|
||||
self.clip_from(start, viewport, translation);
|
||||
std::mem::swap(&mut self.regions, &mut self.input);
|
||||
}
|
||||
|
||||
fn clip_from(&mut self, start: usize, viewport: Rectangle, translation: Vector) {
|
||||
for region in &mut self.regions[start..] {
|
||||
let bounds = Rectangle {
|
||||
x: region[0] - translation.x,
|
||||
y: region[1] - translation.y,
|
||||
width: region[2],
|
||||
height: region[3],
|
||||
};
|
||||
let bounds = bounds.intersection(&viewport).unwrap_or_default();
|
||||
region[..4].copy_from_slice(&[bounds.x, bounds.y, bounds.width, bounds.height]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl widget::Operation for Collect<'_> {
|
||||
fn traverse(&mut self, children: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||
children(self);
|
||||
}
|
||||
|
||||
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
|
||||
let opacity = self
|
||||
.materials
|
||||
.iter()
|
||||
.find(|(target, _)| Some(target) == id)
|
||||
.map(|(_, opacity)| *opacity);
|
||||
if opacity.is_some() || id == Some(&island_id()) || id == Some(&input_id()) {
|
||||
self.input
|
||||
.push([bounds.x, bounds.y, bounds.width, bounds.height, self.radius]);
|
||||
}
|
||||
if opacity.is_some() || id == Some(&island_id()) {
|
||||
self.regions
|
||||
.push([bounds.x, bounds.y, bounds.width, bounds.height, self.radius]);
|
||||
self.opacities.push(opacity.unwrap_or(1.));
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_operation(&mut self, _id: Option<&widget::Id>) {
|
||||
// Iced visits scrollable content before its scrollable callback. Record
|
||||
// the range so only that subtree receives its offset and viewport clip.
|
||||
self.scroll_starts.push((self.regions.len(), self.input.len()));
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
_id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
_content: Rectangle,
|
||||
translation: Vector,
|
||||
_state: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
if let Some((start, input_start)) = self.scroll_starts.pop() {
|
||||
self.clip_from(start, bounds, translation);
|
||||
self.clip_input_from(input_start, bounds, translation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmic::iced::advanced::{clipboard, renderer::Headless};
|
||||
use cosmic::iced::{Font, Pixels, widget as widgets};
|
||||
use cosmic::widget::{container, row};
|
||||
|
||||
#[test]
|
||||
fn clipping_preserves_group_opacity_and_transparent_groups_keep_input() {
|
||||
use widget::Operation;
|
||||
let ids = [widget::Id::unique(), widget::Id::unique(), widget::Id::unique()];
|
||||
let materials = [(ids[0].clone(), 0.2), (ids[1].clone(), 0.), (ids[2].clone(), 0.7)];
|
||||
let mut collector = Collect {
|
||||
materials: &materials,
|
||||
radius: 12.,
|
||||
..Default::default()
|
||||
};
|
||||
for (id, x) in ids.iter().zip([-80., 20., 80.]) {
|
||||
collector.container(Some(id), Rectangle::new((x, 0.).into(), (30., 28.).into()));
|
||||
}
|
||||
let viewport = Rectangle::new((0., 0.).into(), (100., 28.).into());
|
||||
collector.clip_from(0, viewport, Vector::ZERO);
|
||||
collector.clip_input_from(0, viewport, Vector::ZERO);
|
||||
collector.discard_empty();
|
||||
assert_eq!(collector.regions, [[20., 0., 30., 28., 12.], [80., 0., 20., 28., 12.]]);
|
||||
assert_eq!(collector.opacities, [0., 0.7]);
|
||||
assert_eq!(
|
||||
collector.input, collector.regions,
|
||||
"background opacity does not disable controls"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opacity_only_changes_publish_material_without_rebuilding_input() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let renderer = runtime
|
||||
.block_on(<cosmic::Renderer as Headless>::new(
|
||||
Font::default(),
|
||||
Pixels(14.),
|
||||
Some("tiny-skia"),
|
||||
))
|
||||
.unwrap();
|
||||
let viewport = Rectangle::new((0., 0.).into(), (100., 28.).into());
|
||||
let id = widget::Id::unique();
|
||||
let mut tree = None;
|
||||
for (index, opacity) in [1., 0.5, 0.].into_iter().enumerate() {
|
||||
let published = std::cell::RefCell::new(Vec::new());
|
||||
let content = container(widgets::Space::new().width(60).height(28))
|
||||
.id(id.clone())
|
||||
.into();
|
||||
let mut view = frame(
|
||||
content,
|
||||
PanelSurface::None,
|
||||
12.,
|
||||
vec![(id.clone(), opacity)],
|
||||
None,
|
||||
|regions, opacities| published.borrow_mut().push((regions.to_vec(), opacities.to_vec())),
|
||||
|regions| regions,
|
||||
);
|
||||
let tree = tree.get_or_insert_with(|| widget::Tree::new(&view));
|
||||
tree.diff(&mut view);
|
||||
let node = view
|
||||
.as_widget_mut()
|
||||
.layout(tree, &renderer, &layout::Limits::new(Size::ZERO, viewport.size()));
|
||||
let mut messages = Vec::new();
|
||||
view.as_widget_mut().update(
|
||||
tree,
|
||||
&Event::Window(cosmic::iced::window::Event::RedrawRequested(std::time::Instant::now())),
|
||||
Layout::new(&node),
|
||||
mouse::Cursor::Unavailable,
|
||||
&renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut Shell::new(&mut messages),
|
||||
&viewport,
|
||||
);
|
||||
assert_eq!(*published.borrow(), [(vec![[0., 0., 60., 28., 12.]], vec![opacity])]);
|
||||
assert_eq!(messages.len(), usize::from(index == 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measured_islands_and_live_layout_switches_publish_only_changed_bounds() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let renderer = runtime
|
||||
.block_on(<cosmic::Renderer as Headless>::new(
|
||||
Font::default(),
|
||||
Pixels(14.),
|
||||
Some("tiny-skia"),
|
||||
))
|
||||
.unwrap();
|
||||
let viewport = Rectangle::new((0., 0.).into(), (600., 28.).into());
|
||||
let mut tree = None;
|
||||
let theme = crate::ShellTheme {
|
||||
bar_radius: 14.,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for (mode, padding) in [
|
||||
(PanelSurface::None, 12.),
|
||||
(PanelSurface::None, 4.),
|
||||
(PanelSurface::None, 0.),
|
||||
(PanelSurface::Solid, 32.),
|
||||
(PanelSurface::None, 12.),
|
||||
] {
|
||||
let section = |width| {
|
||||
let inner = container(widgets::Space::new().width(width).height(24))
|
||||
.class(cosmic::theme::Container::custom(move |_| {
|
||||
crate::bar::bar_group_style(theme)
|
||||
}))
|
||||
.into();
|
||||
crate::bar::island(
|
||||
inner,
|
||||
theme,
|
||||
crate::panel::PanelGeometry::default(),
|
||||
padding,
|
||||
mode == PanelSurface::None,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
};
|
||||
let islands = row![section(100), section(200),].spacing(60);
|
||||
let content: Element<'_, cosmic::Action<crate::Message>> = container(islands)
|
||||
.padding([0, 12])
|
||||
.width(600)
|
||||
.height(28)
|
||||
.align_y(cosmic::iced::alignment::Vertical::Center)
|
||||
.into();
|
||||
let mut view = frame(
|
||||
content.map(|_| Vec::<[f32; 5]>::new()),
|
||||
mode,
|
||||
14.,
|
||||
vec![],
|
||||
None,
|
||||
|_, _| {},
|
||||
|regions| regions,
|
||||
);
|
||||
let tree = tree.get_or_insert_with(|| widget::Tree::new(&view));
|
||||
tree.diff(&mut view);
|
||||
let node = view
|
||||
.as_widget_mut()
|
||||
.layout(tree, &renderer, &layout::Limits::new(Size::ZERO, viewport.size()));
|
||||
let mut messages = Vec::new();
|
||||
|
||||
for _ in 0..5 {
|
||||
view.as_widget_mut().update(
|
||||
tree,
|
||||
&Event::Window(cosmic::iced::window::Event::RedrawRequested(std::time::Instant::now())),
|
||||
Layout::new(&node),
|
||||
mouse::Cursor::Unavailable,
|
||||
&renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut Shell::new(&mut messages),
|
||||
&viewport,
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(messages.len(), 1, "unchanged frames must not rebuild input regions");
|
||||
let expected = if mode == PanelSurface::None {
|
||||
vec![
|
||||
[12., 0., 100. + 2. * padding, 28., 14.],
|
||||
[172. + 2. * padding, 0., 200. + 2. * padding, 28., 14.],
|
||||
]
|
||||
} else {
|
||||
vec![[0., 0., 600., 28., 14.]]
|
||||
};
|
||||
let regions = &messages[0];
|
||||
assert_eq!(*regions, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrolling_clips_only_its_own_islands_and_preserves_fractional_positions() {
|
||||
let mut collector = Collect {
|
||||
regions: vec![[400., 2., 80., 24., 14.], [10.25, 2., 180., 24., 14.]],
|
||||
input: Vec::new(),
|
||||
scroll_starts: Vec::new(),
|
||||
radius: 14.,
|
||||
..Default::default()
|
||||
};
|
||||
collector.clip_from(
|
||||
1,
|
||||
Rectangle::new((20., 0.).into(), (100., 28.).into()),
|
||||
Vector::new(40.5, 0.),
|
||||
);
|
||||
assert_eq!(
|
||||
collector.regions,
|
||||
vec![[400., 2., 80., 24., 14.], [20., 2., 100., 24., 14.]]
|
||||
);
|
||||
collector.clip_from(0, Rectangle::new((0., 0.).into(), (200., 28.).into()), Vector::ZERO);
|
||||
collector.regions.retain(|region| region[2] > 0. && region[3] > 0.);
|
||||
assert_eq!(collector.regions, vec![[20., 2., 100., 24., 14.]]);
|
||||
}
|
||||
#[test]
|
||||
fn undecorated_controls_have_input_regions_without_creating_material_or_gap_regions() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let renderer = runtime
|
||||
.block_on(<cosmic::Renderer as Headless>::new(
|
||||
Font::default(),
|
||||
Pixels(14.),
|
||||
Some("tiny-skia"),
|
||||
))
|
||||
.unwrap();
|
||||
let content = row![
|
||||
container(widgets::Space::new().width(30).height(24)).id(input_id()),
|
||||
container(widgets::Space::new().width(40).height(24)).id(input_id()),
|
||||
]
|
||||
.spacing(20)
|
||||
.into();
|
||||
let materials = std::cell::RefCell::new(Vec::new());
|
||||
let mut view = frame(
|
||||
content,
|
||||
PanelSurface::None,
|
||||
14.,
|
||||
vec![],
|
||||
None,
|
||||
|regions, _| *materials.borrow_mut() = regions.to_vec(),
|
||||
|regions| regions,
|
||||
);
|
||||
let mut tree = widget::Tree::new(&view);
|
||||
let viewport = Rectangle::new((0., 0.).into(), (100., 28.).into());
|
||||
let node = view
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, viewport.size()));
|
||||
let mut messages = Vec::new();
|
||||
view.as_widget_mut().update(
|
||||
&mut tree,
|
||||
&Event::Window(cosmic::iced::window::Event::RedrawRequested(std::time::Instant::now())),
|
||||
Layout::new(&node),
|
||||
mouse::Cursor::Unavailable,
|
||||
&renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut Shell::new(&mut messages),
|
||||
&viewport,
|
||||
);
|
||||
assert!(materials.borrow().is_empty());
|
||||
assert_eq!(messages, vec![vec![[0., 0., 30., 24., 14.], [50., 0., 40., 24., 14.]]]);
|
||||
let mut collector = Collect {
|
||||
regions: vec![[300., 0., 20., 24., 14.]],
|
||||
input: messages.remove(0),
|
||||
scroll_starts: Vec::new(),
|
||||
radius: 14.,
|
||||
..Default::default()
|
||||
};
|
||||
collector.clip_input_from(
|
||||
1,
|
||||
Rectangle::new((40., 0.).into(), (20., 28.).into()),
|
||||
Vector::new(10., 0.),
|
||||
);
|
||||
assert_eq!(collector.input, vec![[0., 0., 30., 24., 14.], [40., 0., 20., 24., 14.]]);
|
||||
assert_eq!(collector.regions, vec![[300., 0., 20., 24., 14.]]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user