diff --git a/README.md b/README.md index 6b5c4ce..98f6a9f 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,25 @@ The Material You scheme type is Skwd-wall's setting (`matugen.schemeType`). ``` cover_colors = false # true: the open media card and equalizer take the song cover's colours as background transition_ms = 400 # how long a colour change cross-fades; 0 switches instantly +glass = 1.0 # surface opacity, 0.05 to 1; below 1 the desktop shows through ``` +## Glass + +Below `glass = 1.0` the surfaces are see-through. Amane can only blur what the shell draws itself, so the frosting has to come from Hyprland: add this layer rule to the Hyprland config (`hyprland.lua`). The namespace match is a full match, and `ignore_alpha` keeps the transparent margin around the island from being blurred too. + +```lua +-- frosted glass behind amane's island and workspace pills +amane_glass = hl.layer_rule({ + name = "amane-glass", + match = { namespace = "^magic-.*$" }, + blur = true, + ignore_alpha = 0.2, +}) +``` + +The look of the surfaces (fill, rim, highlight, shadow) lives in `src/surface.rs`. + ## Layout ``` @@ -41,6 +58,7 @@ src/spectrum.rs the shown player's sound as 24 levels (pw-record | cava) src/art.rs cover download (curl) and the palette file src/workspaces.rs the workspace pill, top left on every monitor src/theme.rs the shell's colours: the matugen theme file, shell.conf, the cross-fade +src/surface.rs what the island and pills are made of: fill, rim, highlight, shadow src/parse.rs pure text parsing with unit tests, no amane dependency matugen/ the matugen template (amane.colors), rendered by skwd-wall into ./theme shell.conf settings: cover_colors, transition_ms diff --git a/docs/design/base-glass-0.5-card.png b/docs/design/base-glass-0.5-card.png new file mode 100644 index 0000000..a2f307c Binary files /dev/null and b/docs/design/base-glass-0.5-card.png differ diff --git a/docs/design/base-solid-card.png b/docs/design/base-solid-card.png new file mode 100644 index 0000000..d186ddf Binary files /dev/null and b/docs/design/base-solid-card.png differ diff --git a/shell.conf b/shell.conf index 60fb354..ad83052 100644 --- a/shell.conf +++ b/shell.conf @@ -7,3 +7,7 @@ cover_colors = false # how long a colour change cross-fades, in milliseconds; 0 switches instantly transition_ms = 400 + +# how opaque the shell's surfaces are, 0.05 to 1 (1 = solid). below 1 the desktop shows +# through; to frost it, Hyprland has to blur behind the shell (README: Glass) +glass = 1.0 diff --git a/src/island.rs b/src/island.rs index f5b8c77..d5c38b4 100644 --- a/src/island.rs +++ b/src/island.rs @@ -14,6 +14,7 @@ use crate::art::Art; use crate::parse::{self, BANDS, Sink}; use crate::sound::{EQ_PRESETS, MAX_GAIN, Sound}; use crate::spectrum::Spectrum; +use crate::surface; use crate::theme; /* @@ -331,26 +332,16 @@ pub fn view(monitor: &Monitor) -> LayerWindow { Rectangle::new().width(0.0).height(0.0) }; - let shape = Rectangle::new() - .width(width) - .height(height) - .radius(radius) - .fill(backdrop(&colors, 255)) - .border(1.0, Color::blend(theme::now().outline, faded(theme::now().accent, 110), lift)); + // the face centred along the top: while the shape grows it is narrower than the shape + let face = Rectangle::new().width(width).height(height).align_child(Center, Start).child(face); - /* - * one faint pink glow around the island, pill and card alike, stronger while the - * pill is hovered. blur 22 fades out within ~28 px (amane blurs with a deviation - * of blur / 2), inside the window's SHADOW margin. no clip anywhere: neither face - * ever overflows the shape - */ - let shape = shape.shadow(Shadow::drop(theme::now().accent).opacity(mix(0.3, 0.7, lift)).blur(22.0)); + // the surface (fill, rim, glow) is surface.rs's; no clip anywhere: neither face ever overflows the shape + let shape = surface::shell(width, height, radius, &colors, lift, face); let tapped = monitor.name.clone(); let hovered = monitor.name.clone(); let shape = shape - .align_child(Center, Start) .on_click(move |_| tap(&tapped)) .on_hover(move |inside| { if inside { @@ -365,8 +356,7 @@ pub fn view(monitor: &Monitor) -> LayerWindow { if !inside && card_here { collapse(); } - }) - .child(face); + }); let shape = if island.expanded && opened_here { shape } else { shape.cursor(Pointer) }; @@ -551,13 +541,19 @@ fn phases(timeline: f32) -> (f32, f32, f32) { * fades a face in over the island's backdrop by laying the same gradient over it, * thinning as `shown` rises: the same picture as .opacity(shown), without a gpu layer. * the veil spans the whole shape (minus the 1 px rim), so its gradient lines up with - * the backdrop's exactly; amane places gradients by each rectangle's own bounds + * the backdrop's exactly; amane places gradients by each rectangle's own bounds. + * a see-through surface can't hide anything under a veil, so glass fades with + * .opacity(), a gpu layer only while the fade runs */ fn veiled(face: Rectangle, width: f32, height: f32, radius: f32, shown: f32, colors: &[Color; 3]) -> Rectangle { if shown >= 1.0 { return face; } + if theme::glass() < 1.0 { + return Rectangle::new().width(width).height(height).align_child(Center, Start).child(face).opacity(shown); + } + let veil = Rectangle::new() .width(width - 2.0) .height(height - 2.0) @@ -602,17 +598,15 @@ pub fn faded(color: Color, alpha: u8) -> Color { Color::rgba(color.red(), color.green(), color.blue(), alpha) } -// the island while no player is open: a music note and "Nothing playing", glowing like the pill +// the island while no player is open: a music note and "Nothing playing" fn idle(colors: &[Color; 3]) -> Rectangle { - Rectangle::new() - .width(COMPACT_W) - .height(COMPACT_H) - .radius(Full) - .fill(backdrop(colors, 255)) - .border(1.0, theme::now().outline) - .shadow(Shadow::drop(theme::now().accent).opacity(0.3).blur(22.0)) - .align_child(Center, Center) - .child(Row::new(children![icon(Glyph::Note, 15.0, theme::now().accent), Text::new("Nothing playing").size(12.0).color(theme::now().dim)]).gap(8.0).align(Center)) + let content = Rectangle::new().width(COMPACT_W).height(COMPACT_H).align_child(Center, Center).child( + Row::new(children![icon(Glyph::Note, 15.0, theme::now().accent), Text::new("Nothing playing").size(12.0).color(theme::now().dim)]) + .gap(8.0) + .align(Center), + ); + + surface::shell(COMPACT_W, COMPACT_H, COMPACT_H / 2.0, colors, 0.0, content) } // the pill: cover, title, the player's live levels diff --git a/src/main.rs b/src/main.rs index 9064cef..f6b0510 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod island; mod parse; mod sound; mod spectrum; +mod surface; mod theme; mod workspaces; diff --git a/src/surface.rs b/src/surface.rs new file mode 100644 index 0000000..86c6b47 --- /dev/null +++ b/src/surface.rs @@ -0,0 +1,28 @@ +use amane::{Blend, Color, Rectangle, Shadow}; + +use crate::island::{backdrop, faded}; +use crate::theme; + +/* + * what the island, the idle pill and the workspace pill are made of: one place, so + * a design is this file. `shell` wraps `content` (already width x height, with its + * own alignment and padding) in the surface. `lift` (0 to 1) is how hovered it is. + * the default: the gradient of `colors`, a 1 px rim, a soft glow in the accent + */ +pub fn shell(width: f32, height: f32, radius: f32, colors: &[Color; 3], lift: f32, content: Rectangle) -> Rectangle { + let theme = theme::now(); + + Rectangle::new() + .width(width) + .height(height) + .radius(radius) + .fill(backdrop(colors, alpha(theme::glass()))) + .border(1.0, Color::blend(theme.outline, faded(theme.accent, 110), lift)) + .shadow(Shadow::drop(theme.accent).opacity(0.3 + 0.4 * lift).blur(22.0)) + .child(content) +} + +// an opacity, 0 to 1, as a colour's alpha +pub fn alpha(opacity: f32) -> u8 { + (opacity.clamp(0.0, 1.0) * 255.0).round() as u8 +} diff --git a/src/theme.rs b/src/theme.rs index 7d5baea..edf9528 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -73,6 +73,9 @@ pub struct Theme { // the open media card and the equalizer take the cover's colours as their background pub cover_colors: bool, + + // how opaque the shell's surfaces are, 0.05 to 1; below 1 the desktop shows through + glass: f32, } impl Service for Theme { @@ -80,6 +83,7 @@ impl Service for Theme { Self { colors: Animation::new(PINK_ON_PURPLE), cover_colors: false, + glass: 1.0, } } @@ -174,6 +178,7 @@ fn apply(theme: &str, config: &str, startup: bool) { let mut theme = Theme::write(); theme.cover_colors = setting("cover_colors") == Some("true"); + theme.glass = setting("glass").and_then(|glass| glass.parse::().ok()).map_or(1.0, |glass| glass.clamp(0.05, 1.0)); // a change in the middle of a fade starts from the colours on screen now let now = theme.colors.value(); @@ -182,9 +187,10 @@ fn apply(theme: &str, config: &str, startup: bool) { theme.colors.to(colors); } -// the colours of the frame being drawn, set by the view that draws it +// the colours and glass of the frame being drawn, set by the view that draws it thread_local! { static NOW: Cell = const { Cell::new(PINK_ON_PURPLE) }; + static GLASS: Cell = const { Cell::new(1.0) }; } /* @@ -195,6 +201,7 @@ pub fn begin() -> bool { let theme = Theme::read(); NOW.set(theme.colors.value()); + GLASS.set(theme.glass); theme.cover_colors } @@ -202,3 +209,8 @@ pub fn begin() -> bool { pub fn now() -> Colors { NOW.get() } + +// the surfaces' opacity this frame, 0.05 to 1 +pub fn glass() -> f32 { + GLASS.get() +} diff --git a/src/workspaces.rs b/src/workspaces.rs index 000f009..a02e6dc 100644 --- a/src/workspaces.rs +++ b/src/workspaces.rs @@ -5,10 +5,11 @@ use std::time::Duration; use amane::{ Animation, Blend, Center, Color, Full, Horizontal, InputArea, Layer, LayerWindow, Monitor, Padding, Parent, Pointer, - Rectangle, Row, Service, Shadow, Start, Text, Vertical, Weight, Widget, Workspace, Workspaces, Zone, + Rectangle, Row, Service, Start, Text, Vertical, Weight, Widget, Workspace, Workspaces, Zone, }; -use crate::island::{CHIP_HOVER, CLEAR, COMPACT_H, HOVER_MS, SHADOW, TOP_GAP, backdrop, faded}; +use crate::island::{CHIP_HOVER, CLEAR, COMPACT_H, HOVER_MS, SHADOW, TOP_GAP, faded}; +use crate::surface; use crate::theme; // one round slot per workspace, inside the pill's inset; the pill sits LEFT px from the monitor's edge @@ -107,18 +108,9 @@ pub fn view(monitor: &Monitor) -> LayerWindow { )); } - let theme = theme::now(); + let content = Rectangle::new().width(width).height(COMPACT_H).padding(INSET).align_child(Start, Center).child(Row::new(slots).gap(SLOT_GAP).align(Center)); - let pill = Rectangle::new() - .width(width) - .height(COMPACT_H) - .radius(Full) - .fill(backdrop(&theme.surfaces(), 255)) - .border(1.0, theme.outline) - .shadow(Shadow::drop(theme.accent).opacity(0.3).blur(22.0)) - .padding(INSET) - .align_child(Start, Center) - .child(Row::new(slots).gap(SLOT_GAP).align(Center)); + let pill = surface::shell(width, COMPACT_H, COMPACT_H / 2.0, &theme::now().surfaces(), 0.0, content); // clicks next to the pill reach the windows underneath let region = InputArea {