Fix Settings rendering, theme accents and wallpaper loading
This commit is contained in:
1 parent
950ba723ab
commit
49e54f9a63
7 files changed
+454
-31
No files matched your search
@@ -15,6 +15,9 @@ mod visuals;
|
|||||||
mod wallpaper_controls;
|
mod wallpaper_controls;
|
||||||
mod watch;
|
mod watch;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod scroll_tests;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
use super::*;
|
||||||
|
use cosmic::Application;
|
||||||
|
use cosmic::iced::advanced::renderer::{Headless, Renderer as _};
|
||||||
|
use cosmic::iced::advanced::widget::{Operation, Tree};
|
||||||
|
use cosmic::iced::advanced::{Layout, Shell, clipboard, layout, mouse, renderer};
|
||||||
|
use cosmic::iced::{Color, Event, Font, Pixels, Point, Rectangle, Size, Vector, window};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct ScrollPosition {
|
||||||
|
offset: f32,
|
||||||
|
content_height: f32,
|
||||||
|
viewport_height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operation for ScrollPosition {
|
||||||
|
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) {
|
||||||
|
operate(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scrollable(
|
||||||
|
&mut self,
|
||||||
|
id: Option<&cosmic::iced::advanced::widget::Id>,
|
||||||
|
bounds: Rectangle,
|
||||||
|
content: Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
_: &mut dyn cosmic::iced::advanced::widget::operation::Scrollable,
|
||||||
|
) {
|
||||||
|
if id == Some(&widget::Id::new("settings-content")) {
|
||||||
|
self.offset = translation.y;
|
||||||
|
self.content_height = content.height;
|
||||||
|
self.viewport_height = bounds.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "release scroll/render sample; requires headless renderer backends"]
|
||||||
|
fn appearance_scroll_preserves_progress_and_settles_visibility() {
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let bounds = Rectangle::with_size(Size::new(960., 600.));
|
||||||
|
let scale = 1.75;
|
||||||
|
let physical = Size::new(1680, 1050);
|
||||||
|
|
||||||
|
for backend in ["wgpu", "tiny-skia"] {
|
||||||
|
let mut renderer = runtime
|
||||||
|
.block_on(<cosmic::Renderer as Headless>::new(
|
||||||
|
Font::default(),
|
||||||
|
Pixels(14.),
|
||||||
|
Some(backend),
|
||||||
|
))
|
||||||
|
.expect("requested renderer");
|
||||||
|
|
||||||
|
for split in [false, true] {
|
||||||
|
let source = format!("theme {{ split #{split}; family \"ferese-blue\"; }}");
|
||||||
|
let mut app = App::init(
|
||||||
|
Core::default(),
|
||||||
|
(PathBuf::from("/unused/scroll-test.kdl"), Snapshot::parse(source), None),
|
||||||
|
)
|
||||||
|
.0;
|
||||||
|
app.page = Page::Appearance;
|
||||||
|
let theme = app.native_palette.native_theme();
|
||||||
|
let mut tree: Option<Tree> = None;
|
||||||
|
let mut previous = 0.;
|
||||||
|
let mut render_times = Vec::new();
|
||||||
|
let mut visibility_messages = 0;
|
||||||
|
|
||||||
|
for frame in 0..60 {
|
||||||
|
let mut messages = Vec::new();
|
||||||
|
let mut clipboard = clipboard::Null;
|
||||||
|
let mut shell = Shell::new(&mut messages);
|
||||||
|
let cursor = mouse::Cursor::Available(Point::new(680., 300.));
|
||||||
|
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()));
|
||||||
|
let event = if !(4..52).contains(&frame) {
|
||||||
|
Event::Window(window::Event::RedrawRequested(std::time::Instant::now()))
|
||||||
|
} else {
|
||||||
|
Event::Mouse(mouse::Event::WheelScrolled {
|
||||||
|
delta: mouse::ScrollDelta::Pixels {
|
||||||
|
x: 0.,
|
||||||
|
y: if frame < 28 { -12. } else { 12. },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
};
|
||||||
|
view.as_widget_mut().update(
|
||||||
|
tree,
|
||||||
|
&event,
|
||||||
|
Layout::new(&node),
|
||||||
|
cursor,
|
||||||
|
&renderer,
|
||||||
|
&mut clipboard,
|
||||||
|
&mut shell,
|
||||||
|
&bounds,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sensors observe real redraw events as well as wheel input.
|
||||||
|
if (4..52).contains(&frame) {
|
||||||
|
view.as_widget_mut().update(
|
||||||
|
tree,
|
||||||
|
&Event::Window(window::Event::RedrawRequested(std::time::Instant::now())),
|
||||||
|
Layout::new(&node),
|
||||||
|
cursor,
|
||||||
|
&renderer,
|
||||||
|
&mut clipboard,
|
||||||
|
&mut shell,
|
||||||
|
&bounds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut position = ScrollPosition::default();
|
||||||
|
view.as_widget_mut()
|
||||||
|
.operate(tree, Layout::new(&node), &renderer, &mut position);
|
||||||
|
assert!(position.content_height > position.viewport_height);
|
||||||
|
if (4..28).contains(&frame) {
|
||||||
|
assert!(
|
||||||
|
position.offset > previous
|
||||||
|
|| position.offset >= position.content_height - position.viewport_height - 0.5,
|
||||||
|
"scroll stopped at {position:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (28..52).contains(&frame) {
|
||||||
|
assert!(
|
||||||
|
position.offset < previous || position.offset == 0.,
|
||||||
|
"reverse scroll stopped at {position:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
previous = position.offset;
|
||||||
|
renderer.reset(bounds);
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
drop(view);
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
std::hint::black_box(Headless::screenshot(
|
||||||
|
&mut renderer,
|
||||||
|
physical,
|
||||||
|
scale,
|
||||||
|
theme.cosmic().bg_color().into(),
|
||||||
|
));
|
||||||
|
if (4..52).contains(&frame) {
|
||||||
|
render_times.push(started.elapsed().as_micros());
|
||||||
|
}
|
||||||
|
|
||||||
|
for message in messages {
|
||||||
|
if matches!(message, Message::RowVisibility(..)) {
|
||||||
|
visibility_messages += 1;
|
||||||
|
assert!(frame < 56, "visibility kept changing after scrolling stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = app.update(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_times.sort_unstable();
|
||||||
|
eprintln!(
|
||||||
|
"backend={backend} split={split} render_median_us={} p95_us={} visibility_messages={visibility_messages}",
|
||||||
|
render_times[render_times.len() / 2],
|
||||||
|
render_times[render_times.len() * 95 / 100]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,7 +84,7 @@ pub fn choose_family(id: &str, appearance: Option<ferese_config::theme::Appearan
|
|||||||
let kinds: &[&str] = match appearance {
|
let kinds: &[&str] = match appearance {
|
||||||
Some(ferese_config::theme::Appearance::Light) => &["light"],
|
Some(ferese_config::theme::Appearance::Light) => &["light"],
|
||||||
Some(ferese_config::theme::Appearance::Dark) => &["dark"],
|
Some(ferese_config::theme::Appearance::Dark) => &["dark"],
|
||||||
None => &[],
|
None => &["light", "dark"],
|
||||||
};
|
};
|
||||||
let mut edits = vec![set(
|
let mut edits = vec![set(
|
||||||
&appearance.map_or_else(
|
&appearance.map_or_else(
|
||||||
@@ -333,6 +333,63 @@ mod tests {
|
|||||||
assert_eq!(family_selection(&snapshot, Some(Dark)), "catppuccin");
|
assert_eq!(family_selection(&snapshot, Some(Dark)), "catppuccin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shared_family_selection_clears_stale_scoped_gradients() {
|
||||||
|
let source = r##"theme {
|
||||||
|
family "ferese-blue"
|
||||||
|
border { style solid; }
|
||||||
|
light { family "gruvbox"; focus-ring { gradient { from "#3D7BE6"; to "#3D7BE6"; }; }; }
|
||||||
|
dark {
|
||||||
|
family "catppuccin"
|
||||||
|
colors { accent "#3D7BE6"; }
|
||||||
|
focus-ring { gradient { from "#3D7BE6"; to "#3D7BE6"; }; }
|
||||||
|
background { path "/wallpaper.png"; }
|
||||||
|
}
|
||||||
|
}"##;
|
||||||
|
for (id, _, _, _) in ferese_config::families::BUILTINS {
|
||||||
|
let mut snapshot = Snapshot::parse(source.into()).unwrap();
|
||||||
|
for edit in choose_family(id, None) {
|
||||||
|
snapshot.edit(&edit).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
family_selection(&snapshot, Some(ferese_config::theme::Appearance::Light)),
|
||||||
|
"gruvbox"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
family_selection(&snapshot, Some(ferese_config::theme::Appearance::Dark)),
|
||||||
|
"catppuccin"
|
||||||
|
);
|
||||||
|
assert_eq!(snapshot.string("theme.dark.background.path", ""), "/wallpaper.png");
|
||||||
|
assert_eq!(snapshot.string("theme.border.style", ""), "solid");
|
||||||
|
for mode in ["light", "dark"] {
|
||||||
|
assert!(snapshot.item(&format!("theme.{mode}.focus_ring")).is_none());
|
||||||
|
snapshot.edit(&set("theme.mode", mode)).unwrap();
|
||||||
|
let reference = Snapshot::parse(format!(
|
||||||
|
"theme {{ family \"{id}\"; mode \"{mode}\"; border {{ style solid; }}; }}"
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
let resolve = |snapshot: &Snapshot| {
|
||||||
|
ferese_config::theme::resolve(
|
||||||
|
&snapshot.doc,
|
||||||
|
std::path::Path::new("/config"),
|
||||||
|
"2026-10-04T12:00:00Z".parse().unwrap(),
|
||||||
|
|_| unreachable!(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.theme
|
||||||
|
};
|
||||||
|
let actual = resolve(&snapshot);
|
||||||
|
let expected = resolve(&reference);
|
||||||
|
assert_eq!(actual.tokens.focus_ring, expected.tokens.focus_ring, "{id}/{mode}");
|
||||||
|
assert_eq!(
|
||||||
|
Palette::from_resolved(&actual).accent_gradient,
|
||||||
|
Palette::from_resolved(&expected).accent_gradient
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_first_split_starts_with_both_variants_of_the_single_family() {
|
fn the_first_split_starts_with_both_variants_of_the_single_family() {
|
||||||
use ferese_config::theme::Appearance::{Dark, Light};
|
use ferese_config::theme::Appearance::{Dark, Light};
|
||||||
|
|||||||
@@ -60,18 +60,21 @@ struct Preview {
|
|||||||
|
|
||||||
impl Preview {
|
impl Preview {
|
||||||
fn request(&mut self, path: &str) -> bool {
|
fn request(&mut self, path: &str) -> bool {
|
||||||
|
if path.is_empty() {
|
||||||
|
self.path.clear();
|
||||||
|
self.requested = None;
|
||||||
|
self.handle = None;
|
||||||
|
self.error = None;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if self.requested.is_some() || self.path == path {
|
if self.requested.is_some() || self.path == path {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.handle = None;
|
// Keep the last decoded image while its replacement loads. A stale
|
||||||
|
// completion can then also return to that image without decoding again.
|
||||||
self.error = None;
|
self.error = None;
|
||||||
if path.is_empty() {
|
|
||||||
self.path.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.path.clear();
|
|
||||||
self.requested = Some(path.to_owned());
|
self.requested = Some(path.to_owned());
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -317,25 +320,26 @@ impl App {
|
|||||||
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||||
let preview = &self.wallpaper.active;
|
let preview = &self.wallpaper.active;
|
||||||
let mut body = column([]).spacing(8).push(self.label("Current wallpaper", 13.));
|
let mut body = column([]).spacing(8).push(self.label("Current wallpaper", 13.));
|
||||||
if let Some(handle) = &preview.handle {
|
let current: Element<'_, Message> = if let Some(handle) = &preview.handle {
|
||||||
body = body.push(
|
|
||||||
widget::container(
|
|
||||||
widget::image(handle.clone())
|
widget::image(handle.clone())
|
||||||
.border_radius(palette.radius.min(10.))
|
.border_radius(palette.radius.min(10.))
|
||||||
.shape(cosmic::iced::border::Shape::Continuous)
|
.shape(cosmic::iced::border::Shape::Continuous)
|
||||||
.width(Length::Shrink)
|
.width(Length::Shrink)
|
||||||
.height(180)
|
.height(180)
|
||||||
.content_fit(cosmic::iced::ContentFit::Contain),
|
.content_fit(cosmic::iced::ContentFit::Contain)
|
||||||
)
|
.into()
|
||||||
.center_x(Length::Fill),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
body = body.push(self.note(if preview.requested.is_some() {
|
self.wallpaper_placeholder(
|
||||||
"Loading wallpaper preview…"
|
if preview.requested.is_some() {
|
||||||
|
"Loading preview…"
|
||||||
} else {
|
} else {
|
||||||
preview.error.as_deref().unwrap_or("No wallpaper image selected.")
|
preview.error.as_deref().unwrap_or("No wallpaper image selected.")
|
||||||
}));
|
},
|
||||||
}
|
palette,
|
||||||
|
180.,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
body = body.push(widget::container(current).height(180).center_x(Length::Fill));
|
||||||
|
|
||||||
body = body.push(
|
body = body.push(
|
||||||
row([])
|
row([])
|
||||||
@@ -374,10 +378,7 @@ impl App {
|
|||||||
.content_fit(cosmic::iced::ContentFit::Cover)
|
.content_fit(cosmic::iced::ContentFit::Cover)
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
widget::container(self.note(preview.error.as_deref().unwrap_or("Loading…")))
|
self.wallpaper_placeholder(preview.error.as_deref().unwrap_or("Loading preview…"), palette, 110.)
|
||||||
.width(Length::Fill)
|
|
||||||
.height(110)
|
|
||||||
.into()
|
|
||||||
};
|
};
|
||||||
let label_color = if preview.handle.is_none() {
|
let label_color = if preview.handle.is_none() {
|
||||||
palette.text
|
palette.text
|
||||||
@@ -421,12 +422,104 @@ impl App {
|
|||||||
.push(defaults)
|
.push(defaults)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wallpaper_placeholder<'a>(
|
||||||
|
&'a self,
|
||||||
|
message: &'a str,
|
||||||
|
palette: visuals::Palette,
|
||||||
|
height: f32,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
widget::container(
|
||||||
|
column([])
|
||||||
|
.spacing(8)
|
||||||
|
.align_x(Alignment::Center)
|
||||||
|
.push(visuals::action_icon(
|
||||||
|
"M3 4h18v16H3z M3 15l5-5 5 5 3-3 5 5 M15 8h.01",
|
||||||
|
palette.muted,
|
||||||
|
))
|
||||||
|
.push(self.note(message)),
|
||||||
|
)
|
||||||
|
.padding(16)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(height)
|
||||||
|
.center_x(Length::Fill)
|
||||||
|
.center_y(height)
|
||||||
|
.class(visuals::surface(palette.card, palette.radius.min(10.)))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preview_slots_keep_their_size_while_loading_and_on_failure() {
|
||||||
|
use cosmic::Application;
|
||||||
|
use cosmic::iced::advanced::renderer::Headless;
|
||||||
|
use cosmic::iced::advanced::{layout, widget::Tree};
|
||||||
|
use cosmic::iced::{Font, Pixels, Size};
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
for width in [420., 760.] {
|
||||||
|
let mut app = App::init(
|
||||||
|
cosmic::app::Core::default(),
|
||||||
|
("/preview/config.kdl".into(), Snapshot::parse(String::new()), None),
|
||||||
|
)
|
||||||
|
.0;
|
||||||
|
let dimensions = |app: &App| {
|
||||||
|
let mut view = app.wallpaper_controls();
|
||||||
|
let mut tree = Tree::new(view.as_widget());
|
||||||
|
view.as_widget_mut()
|
||||||
|
.layout(
|
||||||
|
&mut tree,
|
||||||
|
&renderer,
|
||||||
|
&layout::Limits::new(Size::ZERO, Size::new(width, f32::INFINITY)),
|
||||||
|
)
|
||||||
|
.size()
|
||||||
|
};
|
||||||
|
let initial = dimensions(&app);
|
||||||
|
assert!(app.wallpaper.active.request("/active.png"));
|
||||||
|
|
||||||
|
for preview in &mut app.wallpaper.defaults {
|
||||||
|
assert!(preview.request("/default.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(dimensions(&app), initial);
|
||||||
|
let handle = widget::image::Handle::from_rgba(2, 1, vec![255; 8]);
|
||||||
|
app.wallpaper
|
||||||
|
.active
|
||||||
|
.finish("/active.png".into(), "/active.png", Ok(handle.clone()));
|
||||||
|
|
||||||
|
for index in 0..app.wallpaper.defaults.len() {
|
||||||
|
app.wallpaper.defaults[index].finish("/default.png".into(), "/default.png", Ok(handle.clone()));
|
||||||
|
assert_eq!(dimensions(&app), initial);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(app.wallpaper.active.request("/missing.png"));
|
||||||
|
assert_eq!(dimensions(&app), initial);
|
||||||
|
app.wallpaper.active.finish(
|
||||||
|
"/missing.png".into(),
|
||||||
|
"/missing.png",
|
||||||
|
Err("Could not load this image.".into()),
|
||||||
|
);
|
||||||
|
assert_eq!(dimensions(&app), initial);
|
||||||
|
assert!(!app.wallpaper.active.request(""));
|
||||||
|
assert_eq!(dimensions(&app), initial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "release UI preview; requires headless renderer backends"]
|
#[ignore = "release UI preview; requires headless renderer backends"]
|
||||||
fn render_preview_pages_on_both_backends() {
|
fn render_preview_pages_on_both_backends() {
|
||||||
@@ -564,14 +657,47 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn returning_to_a_previous_path_during_a_load_restarts_its_preview() {
|
fn returning_to_a_previous_path_during_a_load_reuses_its_preview() {
|
||||||
let mut preview = Preview::default();
|
let mut preview = Preview::default();
|
||||||
let image = widget::image::Handle::from_rgba(1, 1, vec![0, 0, 0, 255]);
|
let image = widget::image::Handle::from_rgba(1, 1, vec![0, 0, 0, 255]);
|
||||||
assert!(preview.request("/first.png"));
|
assert!(preview.request("/first.png"));
|
||||||
preview.finish("/first.png".into(), "/first.png", Ok(image.clone()));
|
preview.finish("/first.png".into(), "/first.png", Ok(image.clone()));
|
||||||
assert!(preview.request("/second.png"));
|
assert!(preview.request("/second.png"));
|
||||||
preview.finish("/second.png".into(), "/first.png", Ok(image));
|
preview.finish("/second.png".into(), "/first.png", Ok(image));
|
||||||
|
assert!(!preview.request("/first.png"));
|
||||||
|
assert!(preview.handle.is_some());
|
||||||
|
assert_eq!(preview.path, "/first.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacement_keeps_the_previous_image_until_it_is_ready() {
|
||||||
|
let mut preview = Preview::default();
|
||||||
|
let first = widget::image::Handle::from_rgba(1, 1, vec![0, 0, 0, 255]);
|
||||||
|
let second = widget::image::Handle::from_rgba(1, 1, vec![255, 255, 255, 255]);
|
||||||
assert!(preview.request("/first.png"));
|
assert!(preview.request("/first.png"));
|
||||||
|
preview.finish("/first.png".into(), "/first.png", Ok(first.clone()));
|
||||||
|
assert!(preview.request("/second.png"));
|
||||||
|
assert_eq!(preview.handle.as_ref().unwrap().id(), first.id());
|
||||||
|
assert_eq!(preview.path, "/first.png");
|
||||||
|
assert!(!preview.request("/second.png"));
|
||||||
|
preview.finish("/second.png".into(), "/second.png", Ok(second.clone()));
|
||||||
|
assert_eq!(preview.handle.as_ref().unwrap().id(), second.id());
|
||||||
|
assert_eq!(preview.path, "/second.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clearing_wallpaper_during_a_load_does_not_restore_the_old_image() {
|
||||||
|
let mut preview = Preview::default();
|
||||||
|
let image = widget::image::Handle::from_rgba(1, 1, vec![0, 0, 0, 255]);
|
||||||
|
assert!(preview.request("/first.png"));
|
||||||
|
preview.finish("/first.png".into(), "/first.png", Ok(image.clone()));
|
||||||
|
assert!(preview.request("/second.png"));
|
||||||
|
assert!(!preview.request(""));
|
||||||
|
assert!(preview.handle.is_none());
|
||||||
|
assert!(preview.requested.is_none());
|
||||||
|
preview.finish("/second.png".into(), "", Ok(image));
|
||||||
|
assert!(preview.handle.is_none());
|
||||||
|
assert!(preview.path.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ mod note_store;
|
|||||||
mod notification_ui;
|
mod notification_ui;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
mod recording;
|
mod recording;
|
||||||
|
mod renderer;
|
||||||
mod status;
|
mod status;
|
||||||
mod status_ui;
|
mod status_ui;
|
||||||
mod system_modal;
|
mod system_modal;
|
||||||
@@ -109,13 +110,7 @@ impl From<ShellTheme> for BarMetrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> cosmic::iced::Result {
|
fn main() -> cosmic::iced::Result {
|
||||||
// Small shell surfaces do not need a second GPU device and shader setup.
|
renderer::configure_shell();
|
||||||
// Keep wgpu as a fallback and honor an explicit renderer preference.
|
|
||||||
if std::env::var_os("ICED_BACKEND").is_none() {
|
|
||||||
// SAFETY: this is the process entry point, before any worker or
|
|
||||||
// toolkit threads are started.
|
|
||||||
unsafe { std::env::set_var("ICED_BACKEND", "tiny-skia,wgpu") };
|
|
||||||
}
|
|
||||||
|
|
||||||
cosmic::iced::advanced::graphics::text::font_system()
|
cosmic::iced::advanced::graphics::text::font_system()
|
||||||
.write()
|
.write()
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::process::Command;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
static APP_BACKEND: OnceLock<Option<OsString>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Called at process entry, before the toolkit or workers start threads.
|
||||||
|
pub(crate) fn configure_shell() {
|
||||||
|
let backend = std::env::var_os("ICED_BACKEND");
|
||||||
|
let use_shell_default = backend.is_none();
|
||||||
|
APP_BACKEND.set(backend).expect("renderer configured once");
|
||||||
|
|
||||||
|
if use_shell_default {
|
||||||
|
// Small shell surfaces do not need a second GPU device and shader setup.
|
||||||
|
// SAFETY: main calls this before starting any threads.
|
||||||
|
unsafe { std::env::set_var("ICED_BACKEND", "tiny-skia,wgpu") };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure_app(command: &mut Command) {
|
||||||
|
if let Some(backend) = APP_BACKEND.get() {
|
||||||
|
apply_app_backend(command, backend.as_deref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_app_backend(command: &mut Command, backend: Option<&OsStr>) {
|
||||||
|
if let Some(backend) = backend {
|
||||||
|
command.env("ICED_BACKEND", backend);
|
||||||
|
} else {
|
||||||
|
// The shell's automatic software preference must not leak into Settings.
|
||||||
|
command.env_remove("ICED_BACKEND");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_does_not_inherit_the_shells_automatic_software_renderer() {
|
||||||
|
let mut command = Command::new("ferese-settings");
|
||||||
|
command.env("ICED_BACKEND", "tiny-skia,wgpu");
|
||||||
|
apply_app_backend(&mut command, None);
|
||||||
|
assert_eq!(
|
||||||
|
command.get_envs().find(|(key, _)| *key == "ICED_BACKEND").unwrap().1,
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_preserves_explicit_renderer_preferences() {
|
||||||
|
for backend in ["tiny-skia", "wgpu", "wgpu,tiny-skia"] {
|
||||||
|
let mut command = Command::new("ferese-settings");
|
||||||
|
apply_app_backend(&mut command, Some(OsStr::new(backend)));
|
||||||
|
assert_eq!(
|
||||||
|
command.get_envs().find(|(key, _)| *key == "ICED_BACKEND").unwrap().1,
|
||||||
|
Some(OsStr::new(backend))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -632,6 +632,7 @@ fn execute(action: &Action, settings: Option<&[String]>) -> Result<(), String> {
|
|||||||
command.env("WAYLAND_DISPLAY", display);
|
command.env("WAYLAND_DISPLAY", display);
|
||||||
}
|
}
|
||||||
command.env_remove("FERESE_PUBLIC_WAYLAND_DISPLAY");
|
command.env_remove("FERESE_PUBLIC_WAYLAND_DISPLAY");
|
||||||
|
crate::renderer::configure_app(&mut command);
|
||||||
let mut child = command.spawn().map_err(|e| e.to_string())?;
|
let mut child = command.spawn().map_err(|e| e.to_string())?;
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
|
|||||||
Reference in new issue
Block a user