Separate theme rendering from client subscriptions and materials
This commit is contained in:
1 parent
ccf7497b6f
commit
cc0bddbec1
32 files changed
+428
-143
No files matched your search
Generated
+17
-2
@@ -1717,6 +1717,7 @@ dependencies = [
|
|||||||
"ferese-config",
|
"ferese-config",
|
||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
"ferese-theme",
|
"ferese-theme",
|
||||||
|
"ferese-theme-client",
|
||||||
"image",
|
"image",
|
||||||
"jiff",
|
"jiff",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -1736,6 +1737,7 @@ dependencies = [
|
|||||||
"ferese-config",
|
"ferese-config",
|
||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
"ferese-theme",
|
"ferese-theme",
|
||||||
|
"ferese-theme-client",
|
||||||
"glib",
|
"glib",
|
||||||
"libc",
|
"libc",
|
||||||
"libcosmic",
|
"libcosmic",
|
||||||
@@ -1766,6 +1768,7 @@ dependencies = [
|
|||||||
"ferese-config",
|
"ferese-config",
|
||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
"ferese-theme",
|
"ferese-theme",
|
||||||
|
"ferese-theme-client",
|
||||||
"image",
|
"image",
|
||||||
"libcosmic",
|
"libcosmic",
|
||||||
"notify",
|
"notify",
|
||||||
@@ -1785,6 +1788,7 @@ dependencies = [
|
|||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
"ferese-protocols",
|
"ferese-protocols",
|
||||||
"ferese-theme",
|
"ferese-theme",
|
||||||
|
"ferese-theme-client",
|
||||||
"iced_accessibility",
|
"iced_accessibility",
|
||||||
"jiff",
|
"jiff",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -1803,12 +1807,22 @@ name = "ferese-theme"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ferese-config",
|
"ferese-config",
|
||||||
"ferese-ipc",
|
"ferese-theme-model",
|
||||||
"ferese-protocols",
|
|
||||||
"iced_accessibility",
|
"iced_accessibility",
|
||||||
"jiff",
|
"jiff",
|
||||||
"libcosmic",
|
"libcosmic",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ferese-theme-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"ferese-config",
|
||||||
|
"ferese-ipc",
|
||||||
|
"ferese-protocols",
|
||||||
|
"libcosmic",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
]
|
]
|
||||||
@@ -7457,6 +7471,7 @@ dependencies = [
|
|||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
"ferese-protocols",
|
"ferese-protocols",
|
||||||
"ferese-theme",
|
"ferese-theme",
|
||||||
|
"ferese-theme-client",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"gstreamer",
|
"gstreamer",
|
||||||
"image",
|
"image",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ members = [
|
|||||||
"crates/ferese-core",
|
"crates/ferese-core",
|
||||||
"crates/ferese-config",
|
"crates/ferese-config",
|
||||||
"crates/ferese-theme",
|
"crates/ferese-theme",
|
||||||
|
"crates/ferese-theme-client",
|
||||||
"crates/ferese-theme-model",
|
"crates/ferese-theme-model",
|
||||||
"crates/ferese-ipc",
|
"crates/ferese-ipc",
|
||||||
"crates/ferese-layout",
|
"crates/ferese-layout",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "ferese-theme-client"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ferese-config = { path = "../ferese-config" }
|
||||||
|
ferese-ipc = { path = "../ferese-ipc" }
|
||||||
|
ferese-protocols = { path = "../ferese-protocols", default-features = false, features = ["client"] }
|
||||||
|
libcosmic.workspace = true
|
||||||
|
wayland-client.workspace = true
|
||||||
|
tokio = { version = "1", features = ["rt", "sync", "time"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json.workspace = true
|
||||||
|
tempfile.workspace = true
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
//! Theme subscriptions and window-scoped material bindings for Ferese clients.
|
||||||
|
//! Applications own subscription cancellation and keep bindings within window lifetimes.
|
||||||
|
pub mod material;
|
||||||
|
pub mod service;
|
||||||
@@ -111,3 +111,65 @@ impl Dispatch<FereseSurfaceMaterialV1, ()> for MaterialState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delegate_noop!(MaterialState: ignore FereseMaterialManagerV1);
|
delegate_noop!(MaterialState: ignore FereseMaterialManagerV1);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use cosmic::iced::window::raw_window_handle::{
|
||||||
|
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WaylandDisplayHandle, WaylandWindowHandle,
|
||||||
|
WindowHandle,
|
||||||
|
};
|
||||||
|
use std::ptr::NonNull;
|
||||||
|
use wayland_client::protocol::wl_compositor;
|
||||||
|
|
||||||
|
// Supplies the same borrowed Wayland handles used by Iced, backed by a real client.
|
||||||
|
struct Window {
|
||||||
|
connection: Connection,
|
||||||
|
surface: wl_surface::WlSurface,
|
||||||
|
}
|
||||||
|
impl HasDisplayHandle for Window {
|
||||||
|
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||||
|
let pointer = NonNull::new(self.connection.backend().display_ptr().cast()).unwrap();
|
||||||
|
// The connection outlives the returned handle.
|
||||||
|
Ok(unsafe { DisplayHandle::borrow_raw(WaylandDisplayHandle::new(pointer).into()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl HasWindowHandle for Window {
|
||||||
|
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
||||||
|
let pointer = NonNull::new(self.surface.id().as_ptr().cast()).unwrap();
|
||||||
|
// The surface outlives the returned handle.
|
||||||
|
Ok(unsafe { WindowHandle::borrow_raw(WaylandWindowHandle::new(pointer).into()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_noop!(MaterialState: ignore wl_compositor::WlCompositor);
|
||||||
|
delegate_noop!(MaterialState: ignore wl_surface::WlSurface);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires a private nested Ferese compositor"]
|
||||||
|
fn attachment_acknowledgement_and_last_clone_release() {
|
||||||
|
assert_eq!(std::env::var("FERESE_TEST_MATERIAL").as_deref(), Ok("1"));
|
||||||
|
let connection = Connection::connect_to_env().unwrap();
|
||||||
|
let (globals, mut queue) = registry_queue_init::<MaterialState>(&connection).unwrap();
|
||||||
|
let qh = queue.handle();
|
||||||
|
let compositor: wl_compositor::WlCompositor = globals.bind(&qh, 1..=6, ()).unwrap();
|
||||||
|
let window = Window {
|
||||||
|
surface: compositor.create_surface(&qh, ()),
|
||||||
|
connection,
|
||||||
|
};
|
||||||
|
let binding = ModalMaterial::attach(&window).expect("compositor acknowledges attachment");
|
||||||
|
let retained = binding.clone();
|
||||||
|
let weak = Arc::downgrade(&binding._binding);
|
||||||
|
drop(binding);
|
||||||
|
assert!(weak.upgrade().is_some());
|
||||||
|
drop(retained);
|
||||||
|
assert!(weak.upgrade().is_none());
|
||||||
|
queue.roundtrip(&mut MaterialState::default()).unwrap();
|
||||||
|
// The compositor rejects duplicate materials. Reattachment proves the final
|
||||||
|
// drop destroyed the first material while preserving the borrowed display.
|
||||||
|
let replacement = ModalMaterial::attach(&window).expect("last clone released the surface material");
|
||||||
|
drop(replacement);
|
||||||
|
queue.roundtrip(&mut MaterialState::default()).unwrap();
|
||||||
|
window.surface.destroy();
|
||||||
|
queue.roundtrip(&mut MaterialState::default()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
use cosmic::iced::futures::SinkExt;
|
||||||
|
use ferese_ipc::theme::Snapshot;
|
||||||
|
|
||||||
|
pub fn current() -> Snapshot {
|
||||||
|
ferese_ipc::theme::Connection::connect()
|
||||||
|
.ok()
|
||||||
|
.and_then(|mut connection| connection.get(ferese_config::families::builtins).ok())
|
||||||
|
.unwrap_or_else(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standalone appearance; it does not represent compositor acceptance.
|
||||||
|
pub fn fallback() -> Snapshot {
|
||||||
|
let theme = ferese_config::theme::default_theme();
|
||||||
|
Snapshot {
|
||||||
|
version: ferese_ipc::theme::SCHEMA_VERSION,
|
||||||
|
revision: 0,
|
||||||
|
mode: ferese_config::theme::Mode::Dark,
|
||||||
|
presented: theme.clone(),
|
||||||
|
theme,
|
||||||
|
warnings: Vec::new(),
|
||||||
|
error: None,
|
||||||
|
families: ferese_config::families::builtins(),
|
||||||
|
fallback_note: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscription() -> cosmic::iced::Subscription<Snapshot> {
|
||||||
|
cosmic::iced::Subscription::run(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stream() -> impl cosmic::iced::futures::Stream<Item = Snapshot> {
|
||||||
|
cosmic::iced::stream::channel(1, async |mut output| {
|
||||||
|
loop {
|
||||||
|
let connection = tokio::task::spawn_blocking(ferese_ipc::theme::Connection::connect).await;
|
||||||
|
let Ok(Ok(mut connection)) = connection else {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(cancellation) = connection.cancellation() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (send, mut receive) = tokio::sync::watch::channel(None);
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let Ok(mut snapshot) = connection.get(ferese_config::families::builtins) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
let revision = snapshot.revision;
|
||||||
|
if send.send(Some(snapshot)).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match connection.watch(revision, ferese_config::families::builtins) {
|
||||||
|
Ok(next) => snapshot = next,
|
||||||
|
Err(_) => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
while receive.changed().await.is_ok() {
|
||||||
|
let snapshot = receive.borrow_and_update().clone();
|
||||||
|
if let Some(snapshot) = snapshot
|
||||||
|
&& output.send(snapshot).await.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(cancellation);
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use cosmic::iced::futures::StreamExt;
|
||||||
|
use ferese_ipc::{Request, Response, read_frame, write_frame};
|
||||||
|
use std::os::unix::net::UnixListener;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subscription_reconnects_and_drop_cancels_a_pending_watch() {
|
||||||
|
const CHILD: &str = "FERESE_THEME_SUBSCRIPTION_TEST";
|
||||||
|
if std::env::var_os(CHILD).is_none() {
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
||||||
|
.args([
|
||||||
|
"--exact",
|
||||||
|
"service::tests::subscription_reconnects_and_drop_cancels_a_pending_watch",
|
||||||
|
"--nocapture",
|
||||||
|
])
|
||||||
|
.env(CHILD, "1")
|
||||||
|
.env("XDG_RUNTIME_DIR", root.path())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"{}\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// No compositor: return the same standalone values without connecting elsewhere.
|
||||||
|
assert_eq!(current(), fallback());
|
||||||
|
let path = ferese_ipc::theme::socket_path().unwrap();
|
||||||
|
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||||
|
let listener = UnixListener::bind(path).unwrap();
|
||||||
|
let (watch_send, watch_receive) = std::sync::mpsc::channel();
|
||||||
|
let server = std::thread::spawn(move || {
|
||||||
|
for revision in [1, 2] {
|
||||||
|
let (mut connection, _) = listener.accept().unwrap();
|
||||||
|
connection.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
|
||||||
|
let get: Request = read_frame(&mut connection).unwrap();
|
||||||
|
assert_eq!(get.command, "theme-get");
|
||||||
|
let mut snapshot = fallback();
|
||||||
|
snapshot.revision = revision;
|
||||||
|
write_frame(
|
||||||
|
&mut connection,
|
||||||
|
&Response::success(get.id, serde_json::to_value(snapshot).unwrap()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let watch: Request = read_frame(&mut connection).unwrap();
|
||||||
|
assert_eq!(watch.command, "theme-watch");
|
||||||
|
assert_eq!(watch.args["since"], revision);
|
||||||
|
if revision == 2 {
|
||||||
|
watch_send.send(()).unwrap();
|
||||||
|
use std::io::Read;
|
||||||
|
// EOF, not the read deadline: dropping the stream must shut down the watch.
|
||||||
|
assert_eq!(connection.read(&mut [0]).unwrap(), 0);
|
||||||
|
}
|
||||||
|
// First connection closes while watched; the stream must reconnect.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_time()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
runtime.block_on(async {
|
||||||
|
let mut subscription = Box::pin(stream());
|
||||||
|
for revision in [1, 2] {
|
||||||
|
let snapshot = tokio::time::timeout(Duration::from_secs(5), subscription.next())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(snapshot.revision, revision);
|
||||||
|
}
|
||||||
|
tokio::task::spawn_blocking(move || watch_receive.recv_timeout(Duration::from_secs(5)).unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
drop(subscription);
|
||||||
|
});
|
||||||
|
server.join().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
+5
-2
@@ -1,5 +1,5 @@
|
|||||||
use ferese_ipc::theme::Snapshot;
|
use ferese_ipc::theme::Snapshot;
|
||||||
use ferese_theme::service::fallback;
|
use ferese_theme_client::service::fallback;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
fn fixture() -> Value {
|
fn fixture() -> Value {
|
||||||
@@ -59,5 +59,8 @@ fn complete_snapshots_do_not_resolve_fallbacks_and_future_versions_are_rejected(
|
|||||||
assert_eq!(serde_json::to_value(snapshot).unwrap(), value);
|
assert_eq!(serde_json::to_value(snapshot).unwrap(), value);
|
||||||
let mut future = value;
|
let mut future = value;
|
||||||
future["version"] = json!(ferese_ipc::theme::SCHEMA_VERSION + 1);
|
future["version"] = json!(ferese_ipc::theme::SCHEMA_VERSION + 1);
|
||||||
assert_eq!(Snapshot::decode(future, Vec::new).unwrap_err(), "Unsupported theme snapshot version");
|
assert_eq!(
|
||||||
|
Snapshot::decode(future, Vec::new).unwrap_err(),
|
||||||
|
"Unsupported theme snapshot version"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -7,12 +7,10 @@ rust-version.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jiff.workspace = true
|
jiff.workspace = true
|
||||||
ferese-config = { path = "../ferese-config" }
|
ferese-theme-model = { path = "../ferese-theme-model" }
|
||||||
libcosmic = { workspace = true, features = ["a11y"] }
|
libcosmic = { workspace = true, features = ["a11y"] }
|
||||||
ferese-protocols = { path = "../ferese-protocols", default-features = false, features = ["client"] }
|
|
||||||
wayland-client.workspace = true
|
|
||||||
ferese-ipc = { path = "../ferese-ipc" }
|
|
||||||
serde_json.workspace = true
|
|
||||||
tokio = { version = "1", features = ["rt", "sync", "time"] }
|
|
||||||
|
|
||||||
iced_accessibility = { git = "https://github.com/pop-os/libcosmic", rev = "03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" }
|
iced_accessibility = { git = "https://github.com/pop-os/libcosmic", rev = "03d7dcb832bc6d15bc1bb07327a1aa426b2d5bc0" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
ferese-config = { path = "../ferese-config" }
|
||||||
@@ -355,7 +355,7 @@ mod tests {
|
|||||||
use cosmic::widget::button::Catalog;
|
use cosmic::widget::button::Catalog;
|
||||||
|
|
||||||
let palette = Palette {
|
let palette = Palette {
|
||||||
appearance: ferese_config::theme::Appearance::Dark,
|
appearance: ferese_theme_model::Appearance::Dark,
|
||||||
high_contrast: false,
|
high_contrast: false,
|
||||||
background: Color::from_rgb8(17, 24, 33),
|
background: Color::from_rgb8(17, 24, 33),
|
||||||
sidebar: Color::from_rgb8(17, 24, 33),
|
sidebar: Color::from_rgb8(17, 24, 33),
|
||||||
@@ -387,8 +387,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn switch_thumb_remains_readable_in_every_preset_and_state() {
|
fn switch_thumb_remains_readable_in_every_preset_and_state() {
|
||||||
for preset in crate::PRESETS {
|
for preset in ferese_config::presets::PRESETS {
|
||||||
let resolved = ferese_config::theme::ResolvedTheme {
|
let resolved = ferese_theme_model::ResolvedTheme {
|
||||||
appearance: preset.appearance,
|
appearance: preset.appearance,
|
||||||
tokens: ferese_config::theme::preset(preset.id, preset.appearance).unwrap(),
|
tokens: ferese_config::theme::preset(preset.id, preset.appearance).unwrap(),
|
||||||
..ferese_config::theme::default_theme()
|
..ferese_config::theme::default_theme()
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use cosmic::iced::advanced::widget::{Id, Operation, Tree, tree};
|
|||||||
use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, renderer};
|
use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, renderer};
|
||||||
use cosmic::iced::{Event, Length, Rectangle, Size, keyboard};
|
use cosmic::iced::{Event, Length, Rectangle, Size, keyboard};
|
||||||
use cosmic::{Element, Renderer, Theme};
|
use cosmic::{Element, Renderer, Theme};
|
||||||
use ferese_config::families::{Family, Palette};
|
use ferese_theme_model::Appearance;
|
||||||
use ferese_config::theme::Appearance;
|
use ferese_theme_model::families::{Family, Palette};
|
||||||
use iced_accessibility::{A11yTree, accesskit};
|
use iced_accessibility::{A11yTree, accesskit};
|
||||||
|
|
||||||
pub struct TileOptions {
|
pub struct TileOptions {
|
||||||
@@ -184,9 +184,9 @@ pub fn svg(family: &Family, variant: Option<Appearance>, active: Option<Appearan
|
|||||||
.or(family.dark.as_ref())
|
.or(family.dark.as_ref())
|
||||||
.or(family.light.as_ref())
|
.or(family.light.as_ref())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let foreground = ferese_config::theme::hex(ferese_config::theme::readable(
|
let foreground = ferese_theme_model::hex(ferese_theme_model::readable(
|
||||||
[1.; 4],
|
[1.; 4],
|
||||||
ferese_config::theme::rgba(&p.accent).unwrap(),
|
ferese_theme_model::rgba(&p.accent).unwrap(),
|
||||||
4.5,
|
4.5,
|
||||||
));
|
));
|
||||||
result.push_str(&format!(r#"<circle cx="183" cy="16" r="10" fill="{}"/><path d="M178 16l3 3 6-6" fill="none" stroke="{foreground}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>"#, p.accent));
|
result.push_str(&format!(r#"<circle cx="183" cy="16" r="10" fill="{}"/><path d="M178 16l3 3 6-6" fill="none" stroke="{foreground}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>"#, p.accent));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Shared colors, typography, controls and compositor materials for Ferese applications.
|
//! Rendering helpers for supplied theme values: colors, typography and controls.
|
||||||
mod button;
|
mod button;
|
||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
mod contrast;
|
mod contrast;
|
||||||
@@ -6,16 +6,12 @@ pub mod controls;
|
|||||||
pub mod gallery;
|
pub mod gallery;
|
||||||
mod geometry;
|
mod geometry;
|
||||||
pub mod icons;
|
pub mod icons;
|
||||||
pub mod material;
|
|
||||||
pub mod menus;
|
pub mod menus;
|
||||||
mod palette;
|
mod palette;
|
||||||
mod presets;
|
|
||||||
pub mod service;
|
|
||||||
mod typography;
|
mod typography;
|
||||||
|
|
||||||
pub use button::accent_button;
|
pub use button::accent_button;
|
||||||
pub use contrast::{accent_color, accent_pair, apply, composite, contrast, foreground, luminance};
|
pub use contrast::{accent_color, accent_pair, apply, composite, contrast, foreground, luminance};
|
||||||
pub use geometry::inner_radius;
|
pub use geometry::inner_radius;
|
||||||
pub use palette::{Palette, mix, parse_color, surface_shade};
|
pub use palette::{Palette, material_opacity, mix, parse_color, surface_shade};
|
||||||
pub use presets::{PRESETS, Preset};
|
|
||||||
pub use typography::{font, text};
|
pub use typography::{font, text};
|
||||||
@@ -2,7 +2,7 @@ use cosmic::iced::Color;
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct Palette {
|
pub struct Palette {
|
||||||
pub appearance: ferese_config::theme::Appearance,
|
pub appearance: ferese_theme_model::Appearance,
|
||||||
pub high_contrast: bool,
|
pub high_contrast: bool,
|
||||||
pub background: Color,
|
pub background: Color,
|
||||||
pub sidebar: Color,
|
pub sidebar: Color,
|
||||||
@@ -16,7 +16,7 @@ pub struct Palette {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Palette {
|
impl Palette {
|
||||||
pub fn from_resolved(theme: &ferese_config::theme::ResolvedTheme) -> Self {
|
pub fn from_resolved(theme: &ferese_theme_model::ResolvedTheme) -> Self {
|
||||||
let color = |value: &str| parse_color(value).expect("validated resolved theme color");
|
let color = |value: &str| parse_color(value).expect("validated resolved theme color");
|
||||||
Self {
|
Self {
|
||||||
appearance: theme.appearance,
|
appearance: theme.appearance,
|
||||||
@@ -46,7 +46,7 @@ impl Palette {
|
|||||||
pub fn native_theme(self) -> cosmic::Theme {
|
pub fn native_theme(self) -> cosmic::Theme {
|
||||||
let rgba = |c: Color| cosmic::cosmic_theme::palette::Srgba::new(c.r, c.g, c.b, 1.);
|
let rgba = |c: Color| cosmic::cosmic_theme::palette::Srgba::new(c.r, c.g, c.b, 1.);
|
||||||
use cosmic::cosmic_theme::ThemeBuilder;
|
use cosmic::cosmic_theme::ThemeBuilder;
|
||||||
use ferese_config::theme::Appearance;
|
use ferese_theme_model::Appearance;
|
||||||
let builder = match (self.appearance, self.high_contrast) {
|
let builder = match (self.appearance, self.high_contrast) {
|
||||||
(Appearance::Light, false) => ThemeBuilder::light(),
|
(Appearance::Light, false) => ThemeBuilder::light(),
|
||||||
(Appearance::Dark, false) => ThemeBuilder::dark(),
|
(Appearance::Dark, false) => ThemeBuilder::dark(),
|
||||||
@@ -109,6 +109,14 @@ pub fn surface_shade(base: Color) -> Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn material_opacity(theme: &ferese_theme_model::ResolvedTheme) -> f32 {
|
||||||
|
if theme.tokens.material.style == "translucent" {
|
||||||
|
theme.tokens.material.opacity as f32
|
||||||
|
} else {
|
||||||
|
1.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -116,11 +124,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn native_theme_preserves_the_resolved_accessibility_mode() {
|
fn native_theme_preserves_the_resolved_accessibility_mode() {
|
||||||
for appearance in [
|
for appearance in [
|
||||||
ferese_config::theme::Appearance::Light,
|
ferese_theme_model::Appearance::Light,
|
||||||
ferese_config::theme::Appearance::Dark,
|
ferese_theme_model::Appearance::Dark,
|
||||||
] {
|
] {
|
||||||
for high_contrast in [false, true] {
|
for high_contrast in [false, true] {
|
||||||
let mut resolved = ferese_config::theme::ResolvedTheme {
|
let mut resolved = ferese_theme_model::ResolvedTheme {
|
||||||
appearance,
|
appearance,
|
||||||
..ferese_config::theme::default_theme()
|
..ferese_config::theme::default_theme()
|
||||||
};
|
};
|
||||||
@@ -134,8 +142,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_preset_builds_an_opaque_control_palette_over_translucent_materials() {
|
fn every_preset_builds_an_opaque_control_palette_over_translucent_materials() {
|
||||||
for preset in crate::PRESETS {
|
for preset in ferese_config::presets::PRESETS {
|
||||||
let mut resolved = ferese_config::theme::ResolvedTheme {
|
let mut resolved = ferese_theme_model::ResolvedTheme {
|
||||||
appearance: preset.appearance,
|
appearance: preset.appearance,
|
||||||
tokens: ferese_config::theme::preset(preset.id, preset.appearance).unwrap(),
|
tokens: ferese_config::theme::preset(preset.id, preset.appearance).unwrap(),
|
||||||
..ferese_config::theme::default_theme()
|
..ferese_config::theme::default_theme()
|
||||||
@@ -145,7 +153,7 @@ mod tests {
|
|||||||
resolved.tokens.geometry.shell_radius = 0.;
|
resolved.tokens.geometry.shell_radius = 0.;
|
||||||
let palette = Palette::from_resolved(&resolved);
|
let palette = Palette::from_resolved(&resolved);
|
||||||
let native = palette.native_theme();
|
let native = palette.native_theme();
|
||||||
assert_eq!(crate::service::opacity(&resolved), 0.25);
|
assert_eq!(crate::material_opacity(&resolved), 0.25);
|
||||||
assert_eq!(native.cosmic().primary(false).base.alpha, 1.);
|
assert_eq!(native.cosmic().primary(false).base.alpha, 1.);
|
||||||
assert_eq!(native.cosmic().corner_radii.radius_m, [0.; 4]);
|
assert_eq!(native.cosmic().corner_radii.radius_m, [0.; 4]);
|
||||||
for component in [&native.cosmic().accent, &native.cosmic().accent_button] {
|
for component in [&native.cosmic().accent, &native.cosmic().accent_button] {
|
||||||
@@ -171,7 +179,7 @@ mod tests {
|
|||||||
let mut theme = ferese_config::theme::default_theme();
|
let mut theme = ferese_config::theme::default_theme();
|
||||||
theme.tokens.material.style = style.into();
|
theme.tokens.material.style = style.into();
|
||||||
theme.tokens.material.opacity = opacity;
|
theme.tokens.material.opacity = opacity;
|
||||||
assert_eq!(crate::service::opacity(&theme), expected);
|
assert_eq!(crate::material_opacity(&theme), expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub use ferese_config::presets::{PRESETS, Preset};
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
use cosmic::iced::futures::SinkExt;
|
|
||||||
use ferese_ipc::theme::Snapshot;
|
|
||||||
|
|
||||||
pub fn current() -> Snapshot {
|
|
||||||
ferese_ipc::theme::Connection::connect()
|
|
||||||
.ok()
|
|
||||||
.and_then(|mut connection| connection.get(ferese_config::families::builtins).ok())
|
|
||||||
.unwrap_or_else(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Standalone appearance; it does not represent compositor acceptance.
|
|
||||||
pub fn fallback() -> Snapshot {
|
|
||||||
let theme = ferese_config::theme::default_theme();
|
|
||||||
Snapshot {
|
|
||||||
version: ferese_ipc::theme::SCHEMA_VERSION,
|
|
||||||
revision: 0,
|
|
||||||
mode: ferese_config::theme::Mode::Dark,
|
|
||||||
presented: theme.clone(),
|
|
||||||
theme,
|
|
||||||
warnings: Vec::new(),
|
|
||||||
error: None,
|
|
||||||
families: ferese_config::families::builtins(),
|
|
||||||
fallback_note: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn opacity(theme: &ferese_config::theme::ResolvedTheme) -> f32 {
|
|
||||||
if theme.tokens.material.style == "translucent" {
|
|
||||||
theme.tokens.material.opacity as f32
|
|
||||||
} else {
|
|
||||||
1.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscription() -> cosmic::iced::Subscription<Snapshot> {
|
|
||||||
cosmic::iced::Subscription::run(stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stream() -> impl cosmic::iced::futures::Stream<Item = Snapshot> {
|
|
||||||
cosmic::iced::stream::channel(1, async |mut output| {
|
|
||||||
loop {
|
|
||||||
let connection = tokio::task::spawn_blocking(ferese_ipc::theme::Connection::connect).await;
|
|
||||||
let Ok(Ok(mut connection)) = connection else {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Ok(cancellation) = connection.cancellation() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let (send, mut receive) = tokio::sync::watch::channel(None);
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
let Ok(mut snapshot) = connection.get(ferese_config::families::builtins) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
loop {
|
|
||||||
let revision = snapshot.revision;
|
|
||||||
if send.send(Some(snapshot)).is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
match connection.watch(revision, ferese_config::families::builtins) {
|
|
||||||
Ok(next) => snapshot = next,
|
|
||||||
Err(_) => return,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
while receive.changed().await.is_ok() {
|
|
||||||
let snapshot = receive.borrow_and_update().clone();
|
|
||||||
if let Some(snapshot) = snapshot
|
|
||||||
&& output.send(snapshot).await.is_err()
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drop(cancellation);
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@ name = "ferese-record"
|
|||||||
path = "src/record_main.rs"
|
path = "src/record_main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-theme-client = { path = "../crates/ferese-theme-client" }
|
||||||
reis = { version = "0.7.1", features = ["calloop"] }
|
reis = { version = "0.7.1", features = ["calloop"] }
|
||||||
calloop.workspace = true
|
calloop.workspace = true
|
||||||
ferese-protocols = { path = "../crates/ferese-protocols", default-features = false, features = ["client"] }
|
ferese-protocols = { path = "../crates/ferese-protocols", default-features = false, features = ["client"] }
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use cosmic::Element;
|
|||||||
use cosmic::app::{Core, Settings, Task};
|
use cosmic::app::{Core, Settings, Task};
|
||||||
use cosmic::iced::{Alignment, Length};
|
use cosmic::iced::{Alignment, Length};
|
||||||
use cosmic::widget::{button, column, container, image, row, scrollable, text_input};
|
use cosmic::widget::{button, column, container, image, row, scrollable, text_input};
|
||||||
use ferese_theme::material::ModalMaterial;
|
|
||||||
use ferese_theme::{Palette, accent_button, controls, text};
|
use ferese_theme::{Palette, accent_button, controls, text};
|
||||||
|
use ferese_theme_client::material::ModalMaterial;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
@@ -101,7 +101,7 @@ impl cosmic::Application for Consent {
|
|||||||
prompt,
|
prompt,
|
||||||
material: None,
|
material: None,
|
||||||
parent: None,
|
parent: None,
|
||||||
palette: Palette::from_resolved(&ferese_theme::service::current().presented),
|
palette: Palette::from_resolved(&ferese_theme_client::service::current().presented),
|
||||||
font,
|
font,
|
||||||
background,
|
background,
|
||||||
};
|
};
|
||||||
@@ -110,7 +110,7 @@ impl cosmic::Application for Consent {
|
|||||||
|
|
||||||
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
||||||
cosmic::iced::Subscription::batch([
|
cosmic::iced::Subscription::batch([
|
||||||
ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
ferese_theme_client::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
||||||
cosmic::iced::event::listen_with(|event, _, id| match event {
|
cosmic::iced::event::listen_with(|event, _, id| match event {
|
||||||
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::Opened(id)),
|
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::Opened(id)),
|
||||||
cosmic::iced::Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
|
cosmic::iced::Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
|
||||||
@@ -128,7 +128,7 @@ impl cosmic::Application for Consent {
|
|||||||
self.palette = Palette::from_resolved(&snapshot.presented).flat();
|
self.palette = Palette::from_resolved(&snapshot.presented).flat();
|
||||||
self.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
|
self.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
|
||||||
self.background = cosmic::iced::Color {
|
self.background = cosmic::iced::Color {
|
||||||
a: ferese_theme::service::opacity(&snapshot.presented),
|
a: ferese_theme::material_opacity(&snapshot.presented),
|
||||||
..self.palette.sidebar
|
..self.palette.sidebar
|
||||||
};
|
};
|
||||||
return cosmic::command::set_theme(self.palette.native_theme());
|
return cosmic::command::set_theme(self.palette.native_theme());
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
pub(crate) type Attachment = Result<
|
pub(crate) type Attachment = Result<
|
||||||
(
|
(
|
||||||
Option<std::sync::Arc<Parent>>,
|
Option<std::sync::Arc<Parent>>,
|
||||||
Result<ferese_theme::material::ModalMaterial, String>,
|
Result<ferese_theme_client::material::ModalMaterial, String>,
|
||||||
),
|
),
|
||||||
String,
|
String,
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ enum Message {
|
|||||||
|
|
||||||
struct Picker {
|
struct Picker {
|
||||||
core: Core,
|
core: Core,
|
||||||
material: Option<ferese_theme::material::ModalMaterial>,
|
material: Option<ferese_theme_client::material::ModalMaterial>,
|
||||||
parent: Option<std::sync::Arc<crate::parent::Parent>>,
|
parent: Option<std::sync::Arc<crate::parent::Parent>>,
|
||||||
prompt: Prompt,
|
prompt: Prompt,
|
||||||
selected: Vec<usize>,
|
selected: Vec<usize>,
|
||||||
@@ -153,7 +153,7 @@ impl cosmic::Application for Picker {
|
|||||||
|
|
||||||
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
||||||
cosmic::iced::Subscription::batch([
|
cosmic::iced::Subscription::batch([
|
||||||
ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
ferese_theme_client::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
||||||
cosmic::iced::event::listen_with(|event, _, id| match event {
|
cosmic::iced::event::listen_with(|event, _, id| match event {
|
||||||
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
|
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
|
||||||
Some(Message::WindowOpened(id))
|
Some(Message::WindowOpened(id))
|
||||||
@@ -169,7 +169,7 @@ impl cosmic::Application for Picker {
|
|||||||
self.palette = ferese_theme::Palette::from_resolved(&snapshot.presented).flat();
|
self.palette = ferese_theme::Palette::from_resolved(&snapshot.presented).flat();
|
||||||
self.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
|
self.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
|
||||||
self.background = cosmic::iced::Color {
|
self.background = cosmic::iced::Color {
|
||||||
a: ferese_theme::service::opacity(&snapshot.presented),
|
a: ferese_theme::material_opacity(&snapshot.presented),
|
||||||
..self.palette.sidebar
|
..self.palette.sidebar
|
||||||
};
|
};
|
||||||
return cosmic::command::set_theme(self.palette.native_theme());
|
return cosmic::command::set_theme(self.palette.native_theme());
|
||||||
@@ -178,7 +178,7 @@ impl cosmic::Application for Picker {
|
|||||||
let parent = self.prompt.parent.clone();
|
let parent = self.prompt.parent.clone();
|
||||||
return cosmic::iced::window::run(id, move |window| {
|
return cosmic::iced::window::run(id, move |window| {
|
||||||
let parent = crate::parent::Parent::attach(window, &parent)?.map(std::sync::Arc::new);
|
let parent = crate::parent::Parent::attach(window, &parent)?.map(std::sync::Arc::new);
|
||||||
Ok((parent, ferese_theme::material::ModalMaterial::attach(window)))
|
Ok((parent, ferese_theme_client::material::ModalMaterial::attach(window)))
|
||||||
})
|
})
|
||||||
.map(|result| cosmic::Action::App(Message::Attached(result)));
|
.map(|result| cosmic::Action::App(Message::Attached(result)));
|
||||||
}
|
}
|
||||||
@@ -387,14 +387,14 @@ pub(crate) fn appearance() -> (
|
|||||||
cosmic::iced::Color,
|
cosmic::iced::Color,
|
||||||
ferese_theme::Palette,
|
ferese_theme::Palette,
|
||||||
) {
|
) {
|
||||||
let snapshot = ferese_theme::service::current();
|
let snapshot = ferese_theme_client::service::current();
|
||||||
let palette = ferese_theme::Palette::from_resolved(&snapshot.presented).flat();
|
let palette = ferese_theme::Palette::from_resolved(&snapshot.presented).flat();
|
||||||
let family = &snapshot.presented.tokens.typography.font_family;
|
let family = &snapshot.presented.tokens.typography.font_family;
|
||||||
(
|
(
|
||||||
palette.native_theme(),
|
palette.native_theme(),
|
||||||
ferese_theme::font(Some(family)),
|
ferese_theme::font(Some(family)),
|
||||||
cosmic::iced::Color {
|
cosmic::iced::Color {
|
||||||
a: ferese_theme::service::opacity(&snapshot.presented),
|
a: ferese_theme::material_opacity(&snapshot.presented),
|
||||||
..palette.sidebar
|
..palette.sidebar
|
||||||
},
|
},
|
||||||
palette,
|
palette,
|
||||||
|
|||||||
@@ -6,15 +6,24 @@ import unittest
|
|||||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def production_dependencies(manifest, workspace_dependencies):
|
||||||
|
dependencies = set()
|
||||||
|
for section in [manifest, *manifest.get("target", {}).values()]:
|
||||||
|
for kind in ("dependencies", "build-dependencies"):
|
||||||
|
for alias, specification in section.get(kind, {}).items():
|
||||||
|
if isinstance(specification, dict) and specification.get("workspace"):
|
||||||
|
specification = workspace_dependencies[alias]
|
||||||
|
name = specification.get("package", alias) if isinstance(specification, dict) else alias
|
||||||
|
dependencies.add(name)
|
||||||
|
return dependencies
|
||||||
|
|
||||||
|
|
||||||
def dependency_graph():
|
def dependency_graph():
|
||||||
workspace = tomllib.loads((ROOT / "Cargo.toml").read_text())
|
workspace = tomllib.loads((ROOT / "Cargo.toml").read_text())["workspace"]
|
||||||
graph = {}
|
graph = {}
|
||||||
for member in workspace["workspace"]["members"]:
|
for member in workspace["members"]:
|
||||||
manifest = tomllib.loads((ROOT / member / "Cargo.toml").read_text())
|
manifest = tomllib.loads((ROOT / member / "Cargo.toml").read_text())
|
||||||
dependencies = set(manifest.get("dependencies", {}))
|
graph[manifest["package"]["name"]] = production_dependencies(manifest, workspace.get("dependencies", {}))
|
||||||
for target in manifest.get("target", {}).values():
|
|
||||||
dependencies.update(target.get("dependencies", {}))
|
|
||||||
graph[manifest["package"]["name"]] = dependencies
|
|
||||||
return graph
|
return graph
|
||||||
|
|
||||||
|
|
||||||
@@ -43,6 +52,26 @@ class ComponentBoundaries(unittest.TestCase):
|
|||||||
def test_ipc_does_not_depend_on_configuration(self):
|
def test_ipc_does_not_depend_on_configuration(self):
|
||||||
self.assertNotIn("ferese-config", reachable(dependency_graph(), "ferese-ipc"))
|
self.assertNotIn("ferese-config", reachable(dependency_graph(), "ferese-ipc"))
|
||||||
|
|
||||||
|
def test_rendering_does_not_load_configuration_or_own_clients(self):
|
||||||
|
dependencies = reachable(dependency_graph(), "ferese-theme")
|
||||||
|
self.assertTrue(dependencies.isdisjoint({
|
||||||
|
"ferese-config", "ferese-ipc", "ferese-theme-client", "ferese-protocols",
|
||||||
|
}), dependencies)
|
||||||
|
|
||||||
|
def test_client_integration_does_not_depend_on_rendering_or_core(self):
|
||||||
|
dependencies = reachable(dependency_graph(), "ferese-theme-client")
|
||||||
|
self.assertTrue(dependencies.isdisjoint({"ferese-theme", "ferese-core"}), dependencies)
|
||||||
|
|
||||||
|
def test_aliases_build_dependencies_and_target_dependencies_are_checked(self):
|
||||||
|
manifest = {
|
||||||
|
"dependencies": {"alias": {"workspace": True}},
|
||||||
|
"build-dependencies": {"builder": {"package": "ferese-config"}},
|
||||||
|
"target": {"cfg(unix)": {"dependencies": {"client": {"package": "ferese-theme-client"}}}},
|
||||||
|
"dev-dependencies": {"ferese-theme": {}},
|
||||||
|
}
|
||||||
|
self.assertEqual(production_dependencies(manifest, {"alias": {"package": "ferese-core"}}),
|
||||||
|
{"ferese-core", "ferese-config", "ferese-theme-client"})
|
||||||
|
|
||||||
def test_transitive_dependencies_are_checked(self):
|
def test_transitive_dependencies_are_checked(self):
|
||||||
self.assertIn("core", reachable({"shell": {"helper"}, "helper": {"core"}}, "shell"))
|
self.assertIn("core", reachable({"shell": {"helper"}, "helper": {"core"}}, "shell"))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""Check client material lifetimes on a disposable nested compositor.
|
||||||
|
|
||||||
|
FERESE_TEST_MATERIAL=1 python3 scripts/tests/test_material_isolated.py
|
||||||
|
Requires built debug compositor binaries, a Wayland host and dbus-daemon.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(os.environ.get("FERESE_TEST_MATERIAL") == "1", "requires a Wayland host")
|
||||||
|
class MaterialLifetime(unittest.TestCase):
|
||||||
|
def test_attachment_and_release(self):
|
||||||
|
with tempfile.TemporaryDirectory(prefix="ferese-material-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
runtime = root / "runtime"
|
||||||
|
runtime.mkdir(mode=0o700)
|
||||||
|
config = root / "config/ferese"
|
||||||
|
config.mkdir(parents=True)
|
||||||
|
(config / "config.kdl").write_text("")
|
||||||
|
host = Path(os.environ["WAYLAND_DISPLAY"])
|
||||||
|
if not host.is_absolute():
|
||||||
|
host = Path(os.environ["XDG_RUNTIME_DIR"]) / host
|
||||||
|
env = dict(os.environ, WAYLAND_DISPLAY=str(host), XDG_RUNTIME_DIR=str(runtime),
|
||||||
|
XDG_CONFIG_HOME=str(root / "config"), XDG_DATA_HOME=str(root / "data"),
|
||||||
|
XDG_STATE_HOME=str(root / "state"), XDG_CACHE_HOME=str(root / "cache"))
|
||||||
|
children = []
|
||||||
|
with (root / "compositor.log").open("w") as log:
|
||||||
|
try:
|
||||||
|
bus = subprocess.Popen(["dbus-daemon", "--session", "--nofork", "--print-address=1"],
|
||||||
|
env=env, stdout=subprocess.PIPE, text=True, start_new_session=True)
|
||||||
|
children.append(bus)
|
||||||
|
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||||
|
compositor = subprocess.Popen([str(ROOT / "target/debug/ferese"), "--backend", "nested"],
|
||||||
|
env=env, stdout=log, stderr=log, start_new_session=True)
|
||||||
|
children.append(compositor)
|
||||||
|
deadline = time.monotonic() + 10
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
self.assertIsNone(compositor.poll(), (root / "compositor.log").read_text())
|
||||||
|
sockets = [p for p in runtime.glob("wayland-*") if p.is_socket()]
|
||||||
|
if sockets and (runtime / "ferese/control.sock").is_socket():
|
||||||
|
break
|
||||||
|
time.sleep(.05)
|
||||||
|
else:
|
||||||
|
self.fail("Nested compositor did not become ready")
|
||||||
|
env["WAYLAND_DISPLAY"] = str(sockets[0])
|
||||||
|
result = subprocess.run([
|
||||||
|
"cargo", "test", "--workspace", "--locked",
|
||||||
|
"material::tests::attachment_acknowledgement_and_last_clone_release",
|
||||||
|
"--", "--ignored", "--exact",
|
||||||
|
], cwd=ROOT, env=env, capture_output=True, text=True, timeout=120)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
self.assertIn("1 passed", result.stdout)
|
||||||
|
self.assertIsNone(compositor.poll())
|
||||||
|
finally:
|
||||||
|
for child in reversed(children):
|
||||||
|
if child.poll() is None:
|
||||||
|
os.killpg(child.pid, signal.SIGTERM)
|
||||||
|
child.wait(timeout=5)
|
||||||
|
if child.stdout:
|
||||||
|
child.stdout.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-theme-client = { path = "../../crates/ferese-theme-client" }
|
||||||
ferese-ipc = { path = "../../crates/ferese-ipc" }
|
ferese-ipc = { path = "../../crates/ferese-ipc" }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
ferese-theme = { path = "../../crates/ferese-theme" }
|
ferese-theme = { path = "../../crates/ferese-theme" }
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ impl Appearance {
|
|||||||
let doc = path
|
let doc = path
|
||||||
.and_then(|path| std::fs::read_to_string(path).ok())
|
.and_then(|path| std::fs::read_to_string(path).ok())
|
||||||
.and_then(|source| Document::parse(&source).ok());
|
.and_then(|source| Document::parse(&source).ok());
|
||||||
let mut theme = ferese_theme::service::current().presented;
|
let mut theme = ferese_theme_client::service::current().presented;
|
||||||
if let Some(path) = path.filter(|path| ferese_config::config_path().as_deref() != Some(*path))
|
if let Some(path) = path.filter(|path| ferese_config::config_path().as_deref() != Some(*path))
|
||||||
&& let Some(document) = &doc
|
&& let Some(document) = &doc
|
||||||
&& let Ok(mut connection) = ferese_ipc::theme::Connection::connect()
|
&& let Ok(mut connection) = ferese_ipc::theme::Connection::connect()
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ impl cosmic::Application for Locker {
|
|||||||
if self.preview {
|
if self.preview {
|
||||||
Subscription::none()
|
Subscription::none()
|
||||||
} else {
|
} else {
|
||||||
ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot)))
|
ferese_theme_client::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot)))
|
||||||
},
|
},
|
||||||
event::listen_with(|event, _, _| match event {
|
event::listen_with(|event, _, _| match event {
|
||||||
Event::PlatformSpecific(PlatformSpecific::Wayland(
|
Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-theme-client = { path = "../../crates/ferese-theme-client" }
|
||||||
ferese-ipc = { path = "../../crates/ferese-ipc" }
|
ferese-ipc = { path = "../../crates/ferese-ipc" }
|
||||||
ferese-config = { path = "../../crates/ferese-config" }
|
ferese-config = { path = "../../crates/ferese-config" }
|
||||||
ferese-theme = { path = "../../crates/ferese-theme" }
|
ferese-theme = { path = "../../crates/ferese-theme" }
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ struct Appearance {
|
|||||||
|
|
||||||
impl Appearance {
|
impl Appearance {
|
||||||
fn load() -> Self {
|
fn load() -> Self {
|
||||||
let snapshot = ferese_theme::service::current();
|
let snapshot = ferese_theme_client::service::current();
|
||||||
Self::from_resolved(&snapshot.presented)
|
Self::from_resolved(&snapshot.presented)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ impl Appearance {
|
|||||||
appearance: theme.appearance,
|
appearance: theme.appearance,
|
||||||
high_contrast: theme.accessibility.increase_contrast,
|
high_contrast: theme.accessibility.increase_contrast,
|
||||||
surface: Color {
|
surface: Color {
|
||||||
a: ferese_theme::service::opacity(theme),
|
a: ferese_theme::material_opacity(theme),
|
||||||
..palette.sidebar
|
..palette.sidebar
|
||||||
},
|
},
|
||||||
text: palette.text,
|
text: palette.text,
|
||||||
@@ -74,7 +74,7 @@ impl Appearance {
|
|||||||
enum Message {
|
enum Message {
|
||||||
ThemeChanged(Box<ferese_ipc::theme::Snapshot>),
|
ThemeChanged(Box<ferese_ipc::theme::Snapshot>),
|
||||||
WindowOpened(cosmic::iced::window::Id),
|
WindowOpened(cosmic::iced::window::Id),
|
||||||
MaterialAttached(Result<ferese_theme::material::ModalMaterial, String>),
|
MaterialAttached(Result<ferese_theme_client::material::ModalMaterial, String>),
|
||||||
Tick,
|
Tick,
|
||||||
Input(String),
|
Input(String),
|
||||||
Submit,
|
Submit,
|
||||||
@@ -91,7 +91,7 @@ impl std::fmt::Debug for Message {
|
|||||||
|
|
||||||
struct Prompt {
|
struct Prompt {
|
||||||
core: Core,
|
core: Core,
|
||||||
material: Option<ferese_theme::material::ModalMaterial>,
|
material: Option<ferese_theme_client::material::ModalMaterial>,
|
||||||
appearance: Appearance,
|
appearance: Appearance,
|
||||||
incoming: Arc<Mutex<VecDeque<PromptEvent>>>,
|
incoming: Arc<Mutex<VecDeque<PromptEvent>>>,
|
||||||
request: AuthenticationRequest,
|
request: AuthenticationRequest,
|
||||||
@@ -302,7 +302,7 @@ impl cosmic::Application for Prompt {
|
|||||||
|
|
||||||
fn subscription(&self) -> Subscription<Message> {
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
Subscription::batch([
|
Subscription::batch([
|
||||||
ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
ferese_theme_client::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
||||||
cosmic::iced::time::every(Duration::from_millis(30)).map(|_| Message::Tick),
|
cosmic::iced::time::every(Duration::from_millis(30)).map(|_| Message::Tick),
|
||||||
cosmic::iced::event::listen_with(|event, _, id| match event {
|
cosmic::iced::event::listen_with(|event, _, id| match event {
|
||||||
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
|
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
|
||||||
@@ -320,7 +320,7 @@ impl cosmic::Application for Prompt {
|
|||||||
return cosmic::command::set_theme(self.appearance.theme());
|
return cosmic::command::set_theme(self.appearance.theme());
|
||||||
}
|
}
|
||||||
Message::WindowOpened(id) => {
|
Message::WindowOpened(id) => {
|
||||||
return cosmic::iced::window::run(id, ferese_theme::material::ModalMaterial::attach)
|
return cosmic::iced::window::run(id, ferese_theme_client::material::ModalMaterial::attach)
|
||||||
.map(|result| cosmic::Action::App(Message::MaterialAttached(result)));
|
.map(|result| cosmic::Action::App(Message::MaterialAttached(result)));
|
||||||
}
|
}
|
||||||
Message::MaterialAttached(result) => {
|
Message::MaterialAttached(result) => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-theme-client = { path = "../../crates/ferese-theme-client" }
|
||||||
ferese-theme = { path = "../../crates/ferese-theme" }
|
ferese-theme = { path = "../../crates/ferese-theme" }
|
||||||
ferese-config = { path = "../../crates/ferese-config" }
|
ferese-config = { path = "../../crates/ferese-config" }
|
||||||
libcosmic = { workspace = true, features = ["wgpu", "a11y", "xdg-portal"] }
|
libcosmic = { workspace = true, features = ["wgpu", "a11y", "xdg-portal"] }
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ impl cosmic::Application for App {
|
|||||||
.into();
|
.into();
|
||||||
let error = initial.as_ref().err().cloned();
|
let error = initial.as_ref().err().cloned();
|
||||||
let current = initial.unwrap_or_else(|_| Snapshot::parse(String::new()).unwrap());
|
let current = initial.unwrap_or_else(|_| Snapshot::parse(String::new()).unwrap());
|
||||||
let resolved = ferese_theme::service::current();
|
let resolved = ferese_theme_client::service::current();
|
||||||
let font = ferese_theme::font(Some(&resolved.presented.tokens.typography.font_family));
|
let font = ferese_theme::font(Some(&resolved.presented.tokens.typography.font_family));
|
||||||
let native_palette = visuals::Palette::from_resolved(&resolved.presented);
|
let native_palette = visuals::Palette::from_resolved(&resolved.presented);
|
||||||
let mut app = Self {
|
let mut app = Self {
|
||||||
@@ -815,7 +815,7 @@ impl cosmic::Application for App {
|
|||||||
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
||||||
cosmic::iced::Subscription::batch([
|
cosmic::iced::Subscription::batch([
|
||||||
cosmic::iced::Subscription::run_with(self.path.clone(), |path| watch::changes(path)),
|
cosmic::iced::Subscription::run_with(self.path.clone(), |path| watch::changes(path)),
|
||||||
ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
ferese_theme_client::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
||||||
if self.page == Page::Displays {
|
if self.page == Page::Displays {
|
||||||
cosmic::iced::time::every(std::time::Duration::from_secs(2)).map(|_| Message::RefreshDisplays)
|
cosmic::iced::time::every(std::time::Duration::from_secs(2)).map(|_| Message::RefreshDisplays)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ fn hex(c: Color) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn native_theme(_snapshot: Option<&Snapshot>) -> cosmic::Theme {
|
pub fn native_theme(_snapshot: Option<&Snapshot>) -> cosmic::Theme {
|
||||||
Palette::from_resolved(&ferese_theme::service::current().presented).native_theme()
|
Palette::from_resolved(&ferese_theme_client::service::current().presented).native_theme()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icon(page: Page, tint: Color) -> svg_icon::Icon {
|
pub fn icon(page: Page, tint: Color) -> svg_icon::Icon {
|
||||||
@@ -41,7 +41,7 @@ pub fn action_icon(path: &str, tint: Color) -> svg_icon::Icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub use ferese_theme::PRESETS;
|
pub use ferese_config::presets::PRESETS;
|
||||||
|
|
||||||
pub fn split(snapshot: &Snapshot) -> bool {
|
pub fn split(snapshot: &Snapshot) -> bool {
|
||||||
snapshot
|
snapshot
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ferese-theme-client = { path = "../../crates/ferese-theme-client" }
|
||||||
ferese-ipc = { path = "../../crates/ferese-ipc" }
|
ferese-ipc = { path = "../../crates/ferese-ipc" }
|
||||||
ferese-theme = { path = "../../crates/ferese-theme" }
|
ferese-theme = { path = "../../crates/ferese-theme" }
|
||||||
ferese-config = { path = "../../crates/ferese-config" }
|
ferese-config = { path = "../../crates/ferese-config" }
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ pub(crate) fn load() -> ShellConfig {
|
|||||||
|
|
||||||
pub(crate) fn parse_source(source: &str) -> Result<ShellConfig, ferese_config::Error> {
|
pub(crate) fn parse_source(source: &str) -> Result<ShellConfig, ferese_config::Error> {
|
||||||
let document = ferese_config::Document::parse(source)?;
|
let document = ferese_config::Document::parse(source)?;
|
||||||
let snapshot = ferese_theme::service::current();
|
let snapshot = ferese_theme_client::service::current();
|
||||||
let mut config = parse_document(
|
let mut config = parse_document(
|
||||||
&document.with_theme(&snapshot.presented),
|
&document.with_theme(&snapshot.presented),
|
||||||
snapshot.presented.appearance,
|
snapshot.presented.appearance,
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ impl cosmic::Application for FereseShell {
|
|||||||
self.control.as_ref().map_or_else(Subscription::none, |control| {
|
self.control.as_ref().map_or_else(Subscription::none, |control| {
|
||||||
control.subscription().map(|_| Message::ControlReady)
|
control.subscription().map(|_| Message::ControlReady)
|
||||||
}),
|
}),
|
||||||
ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
ferese_theme_client::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
||||||
if self
|
if self
|
||||||
.system_modal
|
.system_modal
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|||||||
Reference in new issue
Block a user