glass-base: a glass opacity setting and one place for the surface look

- shell.conf glass (0.05 to 1, default 1 = solid, as before): the
  surfaces' opacity; read live with the theme
- surface.rs: shell() draws the island, the idle pill and the workspace
  pill (fill, rim, shadow), so a design is one file; the default is the
  look so far
- see-through surfaces can't hide content under a veil, so with glass
  below 1 the open/close fades use .opacity() while they run
- README: the Hyprland layer rule that frosts what's behind the shell
  (namespace match is a full match: ^magic-.*$; ignore_alpha keeps the
  transparent margin around the island sharp)
- docs/design: the solid look and glass 0.5 with the rule, for review
This commit is contained in:
server2
2026-10-09 05:20:44 +02:00
parent 036b326b04
commit 54ca75b855
9 changed files with 90 additions and 41 deletions
+18
View File
@@ -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 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 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 ## 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/art.rs cover download (curl) and the palette file
src/workspaces.rs the workspace pill, top left on every monitor 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/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 src/parse.rs pure text parsing with unit tests, no amane dependency
matugen/ the matugen template (amane.colors), rendered by skwd-wall into ./theme matugen/ the matugen template (amane.colors), rendered by skwd-wall into ./theme
shell.conf settings: cover_colors, transition_ms shell.conf settings: cover_colors, transition_ms
Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

+4
View File
@@ -7,3 +7,7 @@ cover_colors = false
# how long a colour change cross-fades, in milliseconds; 0 switches instantly # how long a colour change cross-fades, in milliseconds; 0 switches instantly
transition_ms = 400 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
+21 -27
View File
@@ -14,6 +14,7 @@ use crate::art::Art;
use crate::parse::{self, BANDS, Sink}; use crate::parse::{self, BANDS, Sink};
use crate::sound::{EQ_PRESETS, MAX_GAIN, Sound}; use crate::sound::{EQ_PRESETS, MAX_GAIN, Sound};
use crate::spectrum::Spectrum; use crate::spectrum::Spectrum;
use crate::surface;
use crate::theme; use crate::theme;
/* /*
@@ -331,26 +332,16 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
Rectangle::new().width(0.0).height(0.0) Rectangle::new().width(0.0).height(0.0)
}; };
let shape = Rectangle::new() // the face centred along the top: while the shape grows it is narrower than the shape
.width(width) let face = Rectangle::new().width(width).height(height).align_child(Center, Start).child(face);
.height(height)
.radius(radius)
.fill(backdrop(&colors, 255))
.border(1.0, Color::blend(theme::now().outline, faded(theme::now().accent, 110), lift));
/* // the surface (fill, rim, glow) is surface.rs's; no clip anywhere: neither face ever overflows the shape
* one faint pink glow around the island, pill and card alike, stronger while the let shape = surface::shell(width, height, radius, &colors, lift, face);
* 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));
let tapped = monitor.name.clone(); let tapped = monitor.name.clone();
let hovered = monitor.name.clone(); let hovered = monitor.name.clone();
let shape = shape let shape = shape
.align_child(Center, Start)
.on_click(move |_| tap(&tapped)) .on_click(move |_| tap(&tapped))
.on_hover(move |inside| { .on_hover(move |inside| {
if inside { if inside {
@@ -365,8 +356,7 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
if !inside && card_here { if !inside && card_here {
collapse(); collapse();
} }
}) });
.child(face);
let shape = if island.expanded && opened_here { shape } else { shape.cursor(Pointer) }; 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, * 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. * 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 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 { fn veiled(face: Rectangle, width: f32, height: f32, radius: f32, shown: f32, colors: &[Color; 3]) -> Rectangle {
if shown >= 1.0 { if shown >= 1.0 {
return face; 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() let veil = Rectangle::new()
.width(width - 2.0) .width(width - 2.0)
.height(height - 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) 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 { fn idle(colors: &[Color; 3]) -> Rectangle {
Rectangle::new() let content = Rectangle::new().width(COMPACT_W).height(COMPACT_H).align_child(Center, Center).child(
.width(COMPACT_W) Row::new(children![icon(Glyph::Note, 15.0, theme::now().accent), Text::new("Nothing playing").size(12.0).color(theme::now().dim)])
.height(COMPACT_H) .gap(8.0)
.radius(Full) .align(Center),
.fill(backdrop(colors, 255)) );
.border(1.0, theme::now().outline)
.shadow(Shadow::drop(theme::now().accent).opacity(0.3).blur(22.0)) surface::shell(COMPACT_W, COMPACT_H, COMPACT_H / 2.0, colors, 0.0, content)
.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))
} }
// the pill: cover, title, the player's live levels // the pill: cover, title, the player's live levels
+1
View File
@@ -3,6 +3,7 @@ mod island;
mod parse; mod parse;
mod sound; mod sound;
mod spectrum; mod spectrum;
mod surface;
mod theme; mod theme;
mod workspaces; mod workspaces;
+28
View File
@@ -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
}
+13 -1
View File
@@ -73,6 +73,9 @@ pub struct Theme {
// the open media card and the equalizer take the cover's colours as their background // the open media card and the equalizer take the cover's colours as their background
pub cover_colors: bool, 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 { impl Service for Theme {
@@ -80,6 +83,7 @@ impl Service for Theme {
Self { Self {
colors: Animation::new(PINK_ON_PURPLE), colors: Animation::new(PINK_ON_PURPLE),
cover_colors: false, cover_colors: false,
glass: 1.0,
} }
} }
@@ -174,6 +178,7 @@ fn apply(theme: &str, config: &str, startup: bool) {
let mut theme = Theme::write(); let mut theme = Theme::write();
theme.cover_colors = setting("cover_colors") == Some("true"); theme.cover_colors = setting("cover_colors") == Some("true");
theme.glass = setting("glass").and_then(|glass| glass.parse::<f32>().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 // a change in the middle of a fade starts from the colours on screen now
let now = theme.colors.value(); let now = theme.colors.value();
@@ -182,9 +187,10 @@ fn apply(theme: &str, config: &str, startup: bool) {
theme.colors.to(colors); 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! { thread_local! {
static NOW: Cell<Colors> = const { Cell::new(PINK_ON_PURPLE) }; static NOW: Cell<Colors> = const { Cell::new(PINK_ON_PURPLE) };
static GLASS: Cell<f32> = const { Cell::new(1.0) };
} }
/* /*
@@ -195,6 +201,7 @@ pub fn begin() -> bool {
let theme = Theme::read(); let theme = Theme::read();
NOW.set(theme.colors.value()); NOW.set(theme.colors.value());
GLASS.set(theme.glass);
theme.cover_colors theme.cover_colors
} }
@@ -202,3 +209,8 @@ pub fn begin() -> bool {
pub fn now() -> Colors { pub fn now() -> Colors {
NOW.get() NOW.get()
} }
// the surfaces' opacity this frame, 0.05 to 1
pub fn glass() -> f32 {
GLASS.get()
}
+5 -13
View File
@@ -5,10 +5,11 @@ use std::time::Duration;
use amane::{ use amane::{
Animation, Blend, Center, Color, Full, Horizontal, InputArea, Layer, LayerWindow, Monitor, Padding, Parent, Pointer, 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; use crate::theme;
// one round slot per workspace, inside the pill's inset; the pill sits LEFT px from the monitor's edge // 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() let pill = surface::shell(width, COMPACT_H, COMPACT_H / 2.0, &theme::now().surfaces(), 0.0, content);
.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));
// clicks next to the pill reach the windows underneath // clicks next to the pill reach the windows underneath
let region = InputArea { let region = InputArea {