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
@@ -355,7 +355,7 @@ mod tests {
|
||||
use cosmic::widget::button::Catalog;
|
||||
|
||||
let palette = Palette {
|
||||
appearance: ferese_config::theme::Appearance::Dark,
|
||||
appearance: ferese_theme_model::Appearance::Dark,
|
||||
high_contrast: false,
|
||||
background: Color::from_rgb8(17, 24, 33),
|
||||
sidebar: Color::from_rgb8(17, 24, 33),
|
||||
@@ -387,8 +387,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn switch_thumb_remains_readable_in_every_preset_and_state() {
|
||||
for preset in crate::PRESETS {
|
||||
let resolved = ferese_config::theme::ResolvedTheme {
|
||||
for preset in ferese_config::presets::PRESETS {
|
||||
let resolved = ferese_theme_model::ResolvedTheme {
|
||||
appearance: preset.appearance,
|
||||
tokens: ferese_config::theme::preset(preset.id, preset.appearance).unwrap(),
|
||||
..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::{Event, Length, Rectangle, Size, keyboard};
|
||||
use cosmic::{Element, Renderer, Theme};
|
||||
use ferese_config::families::{Family, Palette};
|
||||
use ferese_config::theme::Appearance;
|
||||
use ferese_theme_model::Appearance;
|
||||
use ferese_theme_model::families::{Family, Palette};
|
||||
use iced_accessibility::{A11yTree, accesskit};
|
||||
|
||||
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.light.as_ref())
|
||||
.unwrap();
|
||||
let foreground = ferese_config::theme::hex(ferese_config::theme::readable(
|
||||
let foreground = ferese_theme_model::hex(ferese_theme_model::readable(
|
||||
[1.; 4],
|
||||
ferese_config::theme::rgba(&p.accent).unwrap(),
|
||||
ferese_theme_model::rgba(&p.accent).unwrap(),
|
||||
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));
|
||||
|
||||
@@ -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;
|
||||
pub mod calendar;
|
||||
mod contrast;
|
||||
@@ -6,16 +6,12 @@ pub mod controls;
|
||||
pub mod gallery;
|
||||
mod geometry;
|
||||
pub mod icons;
|
||||
pub mod material;
|
||||
pub mod menus;
|
||||
mod palette;
|
||||
mod presets;
|
||||
pub mod service;
|
||||
mod typography;
|
||||
|
||||
pub use button::accent_button;
|
||||
pub use contrast::{accent_color, accent_pair, apply, composite, contrast, foreground, luminance};
|
||||
pub use geometry::inner_radius;
|
||||
pub use palette::{Palette, mix, parse_color, surface_shade};
|
||||
pub use presets::{PRESETS, Preset};
|
||||
pub use palette::{Palette, material_opacity, mix, parse_color, surface_shade};
|
||||
pub use typography::{font, text};
|
||||
@@ -1,113 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ferese_protocols::material::v1::client::ferese_material_manager_v1::FereseMaterialManagerV1;
|
||||
use ferese_protocols::material::v1::client::ferese_surface_material_v1::FereseSurfaceMaterialV1;
|
||||
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
||||
use wayland_client::protocol::{wl_registry, wl_surface};
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop};
|
||||
|
||||
/// Keeps a compositor-owned dialog material attached for the window lifetime.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ModalMaterial {
|
||||
_binding: Arc<Binding>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Binding {
|
||||
connection: Connection,
|
||||
manager: FereseMaterialManagerV1,
|
||||
material: FereseSurfaceMaterialV1,
|
||||
}
|
||||
|
||||
impl ModalMaterial {
|
||||
/// Attach while Iced owns the native window; keep its background transparent on success.
|
||||
/// Other compositors return an error so the caller can retain its own surface fill.
|
||||
pub fn attach(window: &dyn cosmic::iced::window::Window) -> Result<Self, String> {
|
||||
use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
||||
|
||||
let display = window.display_handle().map_err(|error| error.to_string())?;
|
||||
let handle = window.window_handle().map_err(|error| error.to_string())?;
|
||||
let (RawDisplayHandle::Wayland(display), RawWindowHandle::Wayland(handle)) =
|
||||
(display.as_raw(), handle.as_raw())
|
||||
else {
|
||||
return Err("Dialog materials require Wayland".into());
|
||||
};
|
||||
// Iced lends live native handles; this backend borrows its connection.
|
||||
let backend =
|
||||
unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) };
|
||||
let connection = Connection::from_backend(backend);
|
||||
let id = unsafe {
|
||||
wayland_client::backend::ObjectId::from_ptr(
|
||||
wl_surface::WlSurface::interface(),
|
||||
handle.surface.as_ptr().cast(),
|
||||
)
|
||||
}
|
||||
.map_err(|error| error.to_string())?;
|
||||
let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
|
||||
let (globals, mut queue) =
|
||||
registry_queue_init::<MaterialState>(&connection).map_err(|error| error.to_string())?;
|
||||
let qh = queue.handle();
|
||||
let manager = globals
|
||||
.bind::<FereseMaterialManagerV1, _, _>(&qh, 1..=1, ())
|
||||
.map_err(|error| error.to_string())?;
|
||||
let material = manager.get_surface_material(&surface, &qh, ());
|
||||
let binding = Binding {
|
||||
connection,
|
||||
manager,
|
||||
material,
|
||||
};
|
||||
let mut state = MaterialState::default();
|
||||
queue.roundtrip(&mut state).map_err(|error| error.to_string())?;
|
||||
if !state.ready {
|
||||
return Err("Dialog material attachment was not acknowledged".into());
|
||||
}
|
||||
Ok(Self {
|
||||
_binding: Arc::new(binding),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Binding {
|
||||
fn drop(&mut self) {
|
||||
self.material.destroy();
|
||||
self.manager.destroy();
|
||||
let _ = self.connection.flush();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MaterialState {
|
||||
ready: bool,
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for MaterialState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_registry: &wl_registry::WlRegistry,
|
||||
_event: wl_registry::Event,
|
||||
_data: &GlobalListContents,
|
||||
_connection: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<FereseSurfaceMaterialV1, ()> for MaterialState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_material: &FereseSurfaceMaterialV1,
|
||||
event: ferese_protocols::material::v1::client::ferese_surface_material_v1::Event,
|
||||
_data: &(),
|
||||
_connection: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if matches!(
|
||||
event,
|
||||
ferese_protocols::material::v1::client::ferese_surface_material_v1::Event::Ready
|
||||
) {
|
||||
state.ready = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate_noop!(MaterialState: ignore FereseMaterialManagerV1);
|
||||
@@ -2,7 +2,7 @@ use cosmic::iced::Color;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Palette {
|
||||
pub appearance: ferese_config::theme::Appearance,
|
||||
pub appearance: ferese_theme_model::Appearance,
|
||||
pub high_contrast: bool,
|
||||
pub background: Color,
|
||||
pub sidebar: Color,
|
||||
@@ -16,7 +16,7 @@ pub struct 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");
|
||||
Self {
|
||||
appearance: theme.appearance,
|
||||
@@ -46,7 +46,7 @@ impl Palette {
|
||||
pub fn native_theme(self) -> cosmic::Theme {
|
||||
let rgba = |c: Color| cosmic::cosmic_theme::palette::Srgba::new(c.r, c.g, c.b, 1.);
|
||||
use cosmic::cosmic_theme::ThemeBuilder;
|
||||
use ferese_config::theme::Appearance;
|
||||
use ferese_theme_model::Appearance;
|
||||
let builder = match (self.appearance, self.high_contrast) {
|
||||
(Appearance::Light, false) => ThemeBuilder::light(),
|
||||
(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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -116,11 +124,11 @@ mod tests {
|
||||
#[test]
|
||||
fn native_theme_preserves_the_resolved_accessibility_mode() {
|
||||
for appearance in [
|
||||
ferese_config::theme::Appearance::Light,
|
||||
ferese_config::theme::Appearance::Dark,
|
||||
ferese_theme_model::Appearance::Light,
|
||||
ferese_theme_model::Appearance::Dark,
|
||||
] {
|
||||
for high_contrast in [false, true] {
|
||||
let mut resolved = ferese_config::theme::ResolvedTheme {
|
||||
let mut resolved = ferese_theme_model::ResolvedTheme {
|
||||
appearance,
|
||||
..ferese_config::theme::default_theme()
|
||||
};
|
||||
@@ -134,8 +142,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_preset_builds_an_opaque_control_palette_over_translucent_materials() {
|
||||
for preset in crate::PRESETS {
|
||||
let mut resolved = ferese_config::theme::ResolvedTheme {
|
||||
for preset in ferese_config::presets::PRESETS {
|
||||
let mut resolved = ferese_theme_model::ResolvedTheme {
|
||||
appearance: preset.appearance,
|
||||
tokens: ferese_config::theme::preset(preset.id, preset.appearance).unwrap(),
|
||||
..ferese_config::theme::default_theme()
|
||||
@@ -145,7 +153,7 @@ mod tests {
|
||||
resolved.tokens.geometry.shell_radius = 0.;
|
||||
let palette = Palette::from_resolved(&resolved);
|
||||
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().corner_radii.radius_m, [0.; 4]);
|
||||
for component in [&native.cosmic().accent, &native.cosmic().accent_button] {
|
||||
@@ -171,7 +179,7 @@ mod tests {
|
||||
let mut theme = ferese_config::theme::default_theme();
|
||||
theme.tokens.material.style = style.into();
|
||||
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;
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user