diff --git a/README.md b/README.md index 0242c68..af8ef2f 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,23 @@ Every setting lives in `~/.config/amane/config.toml`: sizes, timings, text sizes The config window edits the same file: Settings at the foot of the control centre, or `amane ipc call config toggle`. It is a normal window with a page per part of the shell, a page per Hyprland category, and a search over everything. Numbers with a range get a slider and a field, switches toggle, colours show a swatch; a typed value applies on Enter. Reset brings back the default. +### Glass + +`appearance.surface.design` picks what the island, the pills, the tray menu, the control centre and its popups are made of: `solid` (the wallpaper's gradient with an accent glow), `frosted` (a thin tint, a hairline white rim, a sheen along the top, a soft shadow) or `liquid` (clear glass, denser toward the bottom right, a light-catching edge, the accent's glow). Each design's opacity, rim and light are settings of their own (`appearance.solid|frosted|liquid.*`); the look itself is `src/surface.rs`. On see-through glass a face fades with `.opacity()` instead of a veil, a GPU layer only while it fades. + +Amane can only blur what the shell draws, so the frosting comes from Hyprland: a layer rule for the shell's windows (the namespace match is a full match). `ignore_alpha` leaves pixels at or below that opacity unblurred, which keeps the clear margin around the pills sharp; keep it below the design's thinnest part, or that part shows the desktop unblurred (liquid at its default 0.4 is 0.22 in its top left corner). + +```lua +hl.layer_rule({ + name = "amane-glass", + match = { namespace = "^magic-.*$" }, + blur = true, + ignore_alpha = 0.2, +}) +``` + +The blur's strength is Hyprland's own (`decoration.blur.size`, `passes`; also on the config window's Decoration page). + ### Hyprland options The Hyprland pages list every option Hyprland itself describes (`hyprctl descriptions`), with its current value and default, and whether your Hyprland config sets it. A change applies at once (`hl.config`) and is kept in `config.toml` under `[hyprland.*]`; the shell writes those to `~/.config/hypr/amane.lua`. Load that file last in `hyprland.lua`, so the window's values survive a Hyprland reload and win over the rest of your config: @@ -147,6 +164,7 @@ src/png.rs a stdlib png writer, for tray icons sent as pixels src/system.rs what the control centre shows: iwd, wired cards, openvpn, power, the external GPU src/glyph.rs every drawn icon, as a Canvas or as shapes for a shared one src/theme.rs the shell's colours: the matugen theme file, the cross-fade; watches config.toml +src/surface.rs what the island, pills, menus and popups are made of: the solid, frosted and liquid designs src/config.rs every setting, declared once (typed accessors + the window's list), config.toml read and written src/hypr.rs Hyprland's options for the config window: hyprctl descriptions/getoption, hl.config, amane.lua src/settings.rs the config window, `config` IPC diff --git a/src/config.rs b/src/config.rs index c29cc22..7fd78d9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -32,6 +32,8 @@ pub struct Opt { pub help: &'static str, pub kind: Kind, pub range: Option<(f64, f64)>, + // the only words a text setting takes, shown as chips; empty: any text + pub choices: &'static [&'static str], pub default: fn() -> Value, } @@ -170,7 +172,7 @@ pub fn color(text: &str) -> Option { } macro_rules! settings { - ($( $module:ident { $( $group:ident { $( $name:ident : $ty:ty = $default:expr $(, $min:literal ..= $max:literal)? ; $help:literal )* } )* } )*) => { + ($( $module:ident { $( $group:ident { $( $name:ident : $ty:ty $([ $($choice:literal),+ ])? = $default:expr $(, $min:literal ..= $max:literal)? ; $help:literal )* } )* } )*) => { $( pub mod $module { $( pub mod $group { #[allow(unused_imports)] @@ -190,6 +192,7 @@ macro_rules! settings { help: $help, kind: <$ty as Setting>::KIND, range: settings!(@range $($min, $max)?), + choices: &[ $( $($choice),+ )? ], default: || <$ty as Setting>::value(&$default), }, )* )* )* ]; }; @@ -221,6 +224,27 @@ settings! { motion { hover_ms: u64 = 220, 0..=2000; "How long hover highlights take to fade in and out" } + surface { + design: String ["solid", "frosted", "liquid"] = String::from("solid"); "What the island, the pills, the menus, the control centre and its popups are made of: solid (the wallpaper's gradient with an accent glow), frosted (a thin tint, a hairline rim and a sheen) or liquid (clear glass with a light-catching edge). See-through ones need Hyprland's blur behind the shell: README, Glass" + } + solid { + opacity: f32 = 1.0, 0.05..=1.0; "How opaque the solid design is; below 1 the desktop shows through" + glow: f32 = 0.3, 0.0..=1.0; "Strength of the accent glow around each surface" + hover_glow: f32 = 0.7, 0.0..=1.0; "Strength of the glow while hovered" + } + frosted { + opacity: f32 = 0.62, 0.05..=1.0; "How opaque the frosted tint is" + rim: f32 = 0.19, 0.0..=1.0; "Opacity of the hairline white rim" + sheen: f32 = 0.13, 0.0..=1.0; "Opacity of the white highlight along the top edge" + shadow: f32 = 0.24, 0.0..=1.0; "Strength of the soft shadow under each surface" + } + liquid { + opacity: f32 = 0.4, 0.05..=1.0; "How opaque the glass is in its middle; clearer at the top left, denser at the bottom right" + rim: f32 = 0.33, 0.0..=1.0; "Opacity of the light rim" + rim_width: f32 = 1.5, 0.0..=4.0; "Width of the light rim, in pixels" + light: f32 = 0.235, 0.0..=1.0; "Opacity of the light caught at the top left edge" + glow: f32 = 0.22, 0.0..=1.0; "Strength of the accent glow around each surface" + } fallback { accent: Color = Color::rgb(247, 168, 200); "Accent, used until matugen writes ~/.config/amane/theme from the wallpaper" on_accent: Color = Color::rgb(40, 18, 32); "Text and icons on the accent colour, without a theme file" @@ -575,6 +599,10 @@ fn check(path: &str, value: &Value) -> Result<(), String> { return Err(format!("needs {}", kind_words(option))); } + if !option.choices.is_empty() && !value.text().is_some_and(|word| option.choices.contains(&word)) { + return Err(format!("must be one of {}", option.choices.join(", "))); + } + let numbers: Vec = match value { Value::List(items) => items.iter().filter_map(Value::number).collect(), _ => value.number().into_iter().collect(), diff --git a/src/control.rs b/src/control.rs index 9e21bc2..311ac2a 100644 --- a/src/control.rs +++ b/src/control.rs @@ -11,7 +11,7 @@ use std::time::{Duration, Instant}; use amane::{ Animation, Audio, Blend, Bluetooth, Brightness, Button, Canvas, Cap, Center, Color, Column, Easing, End, Full, - Horizontal, InputArea, Layer, LayerWindow, Line, Monitor, Padding, Parent, Pointer, Rectangle, Row, Service, Shadow, + Horizontal, InputArea, Layer, LayerWindow, Line, Monitor, Padding, Parent, Pointer, Rectangle, Row, Service, Shape, SpaceBetween, Stack, Start, Text, Vertical, Weight, Widget, Zone, children, shapes, }; @@ -20,14 +20,14 @@ use crate::config::control as c; use crate::config::island::{animation as island_animation, pill as island_pill}; use crate::edge::{self, Edge}; use crate::glyph::{Glyph, icon}; -use crate::island::{self, CLEAR, backdrop, faded, mix, phases, veiled}; +use crate::island::{self, CLEAR, faded, mix, phases, veiled}; use crate::map::{self, Centre, Link, Node}; use crate::parse; use crate::prompt; use crate::settings; use crate::sound::Sound; use crate::system::{self, Charge, Gpu, Links, Power, VpnState}; -use crate::theme; +use crate::{surface, theme}; // every size and timing is a setting (config.rs: control.*); these follow from them fn menu_w() -> f32 { @@ -452,14 +452,10 @@ pub fn view(monitor: &Monitor) -> LayerWindow { let tapped = monitor.name.clone(); let hovered = monitor.name.clone(); - 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)) - .shadow(Shadow::drop(theme::now().accent).opacity(mix(0.3, 0.7, lift)).blur(22.0)) - .align_child(Center, Start) + let face = Rectangle::new().width(width).height(height).align_child(Center, Start).child(face); + + // the surface is surface.rs's, like the island's + let shape = surface::shell(width, height, radius, &colors, lift, face) .on_click(move |_| tap(&tapped)) .on_hover(move |inside| { { @@ -481,8 +477,7 @@ pub fn view(monitor: &Monitor) -> LayerWindow { if !inside && panel_here { collapse_soon(); } - }) - .child(face); + }); let shape = if control.expanded && opened_here { shape } else { shape.cursor(Pointer) }; diff --git a/src/island.rs b/src/island.rs index 7b7dcb7..12d9ea3 100644 --- a/src/island.rs +++ b/src/island.rs @@ -16,7 +16,7 @@ use crate::glyph::{Glyph, bold_icon, icon}; use crate::parse::{self, Sink}; use crate::sound::{self, Sound}; use crate::spectrum::Spectrum; -use crate::theme; +use crate::{surface, theme}; /* * opening is one linear timeline in two phases, so the shape never resizes with the @@ -317,26 +317,20 @@ 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 c::pill::shadow() margin. no clip anywhere: neither face - * ever overflows the shape + * the surface (fill, rim, glow; stronger while hovered) is surface.rs's. its glow fades + * out inside the window's c::pill::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 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 { @@ -352,8 +346,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) }; @@ -539,13 +532,19 @@ pub 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. + * see-through glass can't hide anything under a veil, so there faces fade with + * .opacity(), a gpu layer only while the fade runs */ pub fn veiled(face: Rectangle, width: f32, height: f32, radius: f32, shown: f32, colors: &[Color; 3]) -> Rectangle { if shown >= 1.0 { return face; } + if surface::clear() { + 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) @@ -590,17 +589,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", on the pill's surface fn idle(colors: &[Color; 3]) -> Rectangle { - Rectangle::new() + let content = Rectangle::new() .width(c::pill::width()) .height(c::pill::height()) - .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(text::body()).color(theme::now().dim)]).gap(8.0).align(Center)) + .child(Row::new(children![icon(Glyph::Note, 15.0, theme::now().accent), Text::new("Nothing playing").size(text::body()).color(theme::now().dim)]).gap(8.0).align(Center)); + + surface::shell(c::pill::width(), c::pill::height(), c::pill::height() / 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 04dc50b..97a0663 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ mod prompt; mod settings; mod sound; mod spectrum; +mod surface; mod system; mod png; mod theme; diff --git a/src/prompt.rs b/src/prompt.rs index d7ec60f..b3975d3 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -14,7 +14,7 @@ use std::time::Duration; use amane::{ Animation, Blend, Center, Color, Column, End, Full, Horizontal, InputArea, Key, Keyboard, Layer, LayerWindow, Margin, - Monitor, Padding, Parent, Pointer, Rectangle, Row, Service, Shadow, Start, Text, TextInput, Vertical, Weight, Widget, + Monitor, Padding, Parent, Pointer, Rectangle, Row, Service, Start, Text, TextInput, Vertical, Weight, Widget, Zone, children, }; @@ -24,10 +24,10 @@ use crate::config::island::pill as island_pill; use crate::config::popups::{layout, timing}; use crate::control::{self, Control}; use crate::glyph::{Glyph, icon}; -use crate::island::{self, backdrop, veiled}; +use crate::island::{self, veiled}; use crate::parse::{self, Pairing}; use crate::system; -use crate::theme; +use crate::{surface, theme}; // the popup's sizes are settings (config.rs: popups.*) fn inner_w() -> f32 { @@ -621,15 +621,8 @@ pub fn view(monitor: &Monitor) -> LayerWindow { let content = Rectangle::new().width(layout::width()).height(card_h).padding(layout::pad()).child(Column::new(rows).gap(layout::gap())); - let card = Rectangle::new() - .width(layout::width()) - .height(card_h) - .radius(22.0) - .fill(backdrop(&colors, 255)) - .border(1.0, Color::blend(theme.outline, theme.accent, 0.5)) - .shadow(Shadow::drop(theme.accent).opacity(0.45).blur(22.0)) - .translate(0.0, 8.0 * (1.0 - appear)) - .child(veiled(content, layout::width(), card_h, 22.0, appear, &colors)); + // raised a little over the control centre: a stronger glow and rim than a resting surface + let card = surface::shell(layout::width(), card_h, 22.0, &colors, 0.375, veiled(content, layout::width(), card_h, 22.0, appear, &colors)).translate(0.0, 8.0 * (1.0 - appear)); // over the middle of the map menu, a little below the control centre's top let (right, top) = (control_pill::right() + (control::map_width(monitor.width as f32) - layout::width()) / 2.0 - island_pill::shadow(), island_pill::top_gap() + 110.0 - island_pill::shadow()); diff --git a/src/settings.rs b/src/settings.rs index 01adbe7..176dee5 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -480,6 +480,14 @@ fn shell_row(option: &'static Opt, width: f32, colors: &Colors) -> Rectangle { Box::new(switch(on, move || say(config::set(option.path, Value::Bool(!on)), format!("{} = {}", option.path, !on)))) } + (Kind::Text, Value::Text(current)) if !option.choices.is_empty() => Box::new(chips( + option + .choices + .iter() + .map(|&word| (String::from(word), current == word, Box::new(move || say(config::set(option.path, Value::Text(String::from(word))), format!("{} = {word}", option.path))) as Box)) + .collect(), + colors, + )), (Kind::Int | Kind::Float, _) if option.range.is_some() => { let (min, max) = option.range.unwrap_or((0.0, 1.0)); let whole = option.kind == Kind::Int; @@ -514,8 +522,6 @@ fn shell_row(option: &'static Opt, width: f32, colors: &Colors) -> Rectangle { help.push_str(&format!(" ยท default {}", config::shown(&(option.default)()))); } - let _ = colors; - row(config::label(option.name()), option.path.to_string(), help, control, reset, width) } @@ -633,14 +639,31 @@ fn slider(fraction: f64, pick: impl Fn(f64) + Clone + 'static) -> Rectangle { } // a Hyprland number with named values: one chip per name +// a Hyprland option's named values as chips fn choices(option: &HyprOption, colors: &Colors) -> Row { let name = option.name; - let chips: Vec> = option - .choices - .iter() - .map(|(label, value)| { - let chosen = option.current == value.to_string(); - let value = value.to_string(); + + chips( + option + .choices + .iter() + .map(|(label, value)| { + let value = value.to_string(); + + (label.replace('_', " "), option.current == value, Box::new(move || hypr::set(name, &value)) as Box) + }) + .collect(), + colors, + ) +} + +// one choice: its label, whether it's the current one, what a click does +type Choice = (String, bool, Box); + +fn chips(items: Vec, colors: &Colors) -> Row { + let chips: Vec> = items + .into_iter() + .map(|(label, chosen, pick)| { let letters = label.chars().count() as f32; Box::new( @@ -651,8 +674,8 @@ fn choices(option: &HyprOption, colors: &Colors) -> Row { .fill(if chosen { colors.accent } else { config::appearance::overlays::chip() }) .align_child(Center, Center) .cursor(Cursor::Pointer) - .on_click(move |_| hypr::set(name, &value)) - .child(Text::new(label.replace('_', " ")).size(config::appearance::text::small()).color(if chosen { colors.on_accent } else { colors.text })), + .on_click(move |_| pick()) + .child(Text::new(label).size(config::appearance::text::small()).color(if chosen { colors.on_accent } else { colors.text })), ) as Box }) .collect(); diff --git a/src/surface.rs b/src/surface.rs new file mode 100644 index 0000000..6d8badb --- /dev/null +++ b/src/surface.rs @@ -0,0 +1,74 @@ +use amane::{Blend, Color, Gradient, Rectangle, Shadow, Stack, children}; + +use crate::config::appearance::{frosted, liquid, solid, surface}; +use crate::island::{backdrop, faded}; +use crate::theme; + +/* + * what the island, its idle pill, the workspace and tray pills, the tray menu, the control + * centre and its popup are made of: one place, so a design is this file. `shell` wraps + * `content` (already width x height, with its own padding and alignment) in the surface. + * `lift` (0 to 1) is how raised it is: hovered, or a popup over the rest. + * see-through designs need Hyprland's blur behind the shell (README: Glass) + */ +pub fn shell(width: f32, height: f32, radius: f32, colors: &[Color; 3], lift: f32, content: Rectangle) -> Rectangle { + let theme = theme::now(); + let base = Rectangle::new().width(width).height(height).radius(radius); + + match surface::design().as_str() { + // after Apple's materials: a thin tint, a hairline light rim (the accent when hovered), + // a soft highlight along the top as if lit from above, one soft low shadow instead of a glow + "frosted" => { + let sheen = inner(width, height, radius).fill(Gradient::linear(180.0, [(0.0, white(frosted::sheen())), (0.3, white(frosted::sheen() * 0.26)), (0.65, white(0.0))])); + + base.fill(backdrop(colors, alpha(frosted::opacity()))) + .border(1.0, Color::blend(white(frosted::rim()), faded(theme.accent, 150), lift)) + .shadow(Shadow::drop(Color::BLACK).opacity(frosted::shadow() + 0.1 * lift).blur(24.0).offset(0.0, 6.0)) + .child(Stack::new(children![sheen, content]).width(width).height(height)) + } + + // clear glass, denser toward the bottom right where the equalizer's labels sit; a bright + // edge where light enters at the top left and leaves at the bottom right; the accent's glow + "liquid" => { + let glass = liquid::opacity(); + let fill = Gradient::linear(150.0, [(0.0, faded(colors[0], alpha(glass * 0.55))), (0.55, faded(colors[1], alpha(glass))), (1.0, faded(colors[2], alpha(glass * 1.6)))]); + + let light = liquid::light(); + let edge = inner(width, height, radius).fill(Gradient::linear( + 135.0, + [(0.0, white(light)), (0.16, white(light * 0.23)), (0.4, white(0.0)), (0.82, white(0.0)), (1.0, white(light * 0.43))], + )); + + base.fill(fill) + .border(liquid::rim_width(), Color::blend(white(liquid::rim()), faded(theme.flame, 200), lift)) + .shadow(Shadow::drop(theme.accent).opacity(liquid::glow() + 0.3 * lift).blur(24.0)) + .child(Stack::new(children![edge, content]).width(width).height(height)) + } + + // the original: the wallpaper's gradient, a 1 px rim, a soft glow in the accent + _ => base + .fill(backdrop(colors, alpha(solid::opacity()))) + .border(1.0, Color::blend(theme.outline, faded(theme.accent, 110), lift)) + .shadow(Shadow::drop(theme.accent).opacity(solid::glow() + (solid::hover_glow() - solid::glow()) * lift).blur(22.0)) + .child(content), + } +} + +// whether the desktop shows through: then a veil can't hide a face, so faces fade with .opacity() +pub fn clear() -> bool { + surface::design() != "solid" || solid::opacity() < 1.0 +} + +// a layer just inside the rim, so the rim stays crisp over it +fn inner(width: f32, height: f32, radius: f32) -> Rectangle { + Rectangle::new().width(width - 2.0).height(height - 2.0).radius((radius - 1.0).max(0.0)).translate(1.0, 1.0) +} + +// an opacity, 0 to 1, as a colour's alpha +fn alpha(opacity: f32) -> u8 { + (opacity.clamp(0.0, 1.0) * 255.0).round() as u8 +} + +fn white(opacity: f32) -> Color { + Color::rgba(255, 255, 255, alpha(opacity)) +} diff --git a/src/tray.rs b/src/tray.rs index 0018383..f146016 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -16,7 +16,7 @@ use std::{env, fs, thread}; use amane::{ Animation, Argument, Blend, Bus, Button, Center, Color, Column, Full, Horizontal, Image, InputArea, Layer, - LayerWindow, Margin, Monitor, Padding, Parent, Pointer, Rectangle, Row, Scroll, Service, Shadow, SpaceBetween, + LayerWindow, Margin, Monitor, Padding, Parent, Pointer, Rectangle, Row, Scroll, Service, SpaceBetween, Start, Text, Value, Vertical, Weight, Widget, Zone, }; @@ -24,8 +24,8 @@ use crate::config::appearance::{motion, overlays, text}; use crate::config::island::pill as island_pill; use crate::config::tray::{menu, pill}; use crate::glyph::{Glyph, icon}; -use crate::island::{CLEAR, backdrop, faded, veiled}; -use crate::{edge, parse, png, theme}; +use crate::island::{CLEAR, faded, veiled}; +use crate::{edge, parse, png, surface, theme}; const WATCHER: &str = "org.kde.StatusNotifierWatcher"; const WATCHER_PATH: &str = "/StatusNotifierWatcher"; @@ -702,17 +702,9 @@ pub fn pill(monitor: &str, left: f32) -> Option<(Rectangle, f32)> { slots.push(Box::new(slot)); } - let pill = Rectangle::new() - .width(width) - .height(island_pill::height()) - .radius(Full) - .fill(backdrop(&theme.surfaces(), 255)) - .border(1.0, theme.outline) - .shadow(Shadow::drop(theme.accent).opacity(0.3).blur(22.0)) - .padding(pill::inset()) - .align_child(Start, Center) - .on_hover(edge::over_bar) - .child(Row::new(slots).gap(pill::slot_gap()).align(Center)); + let content = Rectangle::new().width(width).height(island_pill::height()).padding(pill::inset()).align_child(Start, Center).child(Row::new(slots).gap(pill::slot_gap()).align(Center)); + + let pill = surface::shell(width, island_pill::height(), island_pill::height() / 2.0, &theme.surfaces(), 0.0, content).on_hover(edge::over_bar); Some((pill, width)) } @@ -822,13 +814,8 @@ pub fn menu_view(monitor: &Monitor) -> LayerWindow { let content = Rectangle::new().width(menu::width()).height(height).padding(menu::pad()).child(Column::new(rows)); - let card = Rectangle::new() - .width(menu::width()) - .height(height) - .radius(16.0) - .fill(backdrop(&colors, 255)) - .border(1.0, theme.outline) - .shadow(Shadow::drop(theme.accent).opacity(0.35).blur(22.0)) + // raised a little over the bar + let card = surface::shell(menu::width(), height, 16.0, &colors, 0.125, veiled(content, menu::width(), height, 16.0, appear, &colors)) .translate(0.0, -6.0 * (1.0 - appear)) .on_hover(|inside| { if inside { @@ -839,8 +826,7 @@ pub fn menu_view(monitor: &Monitor) -> LayerWindow { } else { leave_soon(); } - }) - .child(veiled(content, menu::width(), height, 16.0, appear, &colors)); + }); let left = (open.x - 8.0 - island_pill::shadow()).max(0.0); let top = island_pill::top_gap() + island_pill::height() + 6.0 - island_pill::shadow(); diff --git a/src/workspaces.rs b/src/workspaces.rs index 73f1e49..a15cda7 100644 --- a/src/workspaces.rs +++ b/src/workspaces.rs @@ -5,15 +5,15 @@ use std::time::Duration; use amane::{ Animation, Blend, Center, Color, Full, Horizontal, InputArea, Layer, LayerWindow, Monitor, Padding, Parent, Pointer, - Rectangle, Row, Service, Shadow, Stack, Start, Text, Vertical, Weight, Widget, Workspace, Workspaces, Zone, + Rectangle, Row, Service, Stack, Start, Text, Vertical, Weight, Widget, Workspace, Workspaces, Zone, }; use crate::config::appearance::{motion, overlays, text}; use crate::config::island::pill as island_pill; use crate::config::workspaces::pill; use crate::edge::{self, Edge, Part}; -use crate::island::{CLEAR, backdrop, faded, mix}; -use crate::{theme, tray}; +use crate::island::{CLEAR, faded, mix}; +use crate::{surface, theme, tray}; // one round slot per workspace, inside the pill's inset, the pill pill::left() px from the monitor's edge, // the tray pill::tray_gap() after it (config.rs: workspaces.*) @@ -196,17 +196,9 @@ pub fn view(monitor: &Monitor) -> LayerWindow { layers.extend(slots); - let pill = Rectangle::new() - .width(width) - .height(island_pill::height()) - .radius(Full) - .fill(backdrop(&theme.surfaces(), 255)) - .border(1.0, theme.outline) - .shadow(Shadow::drop(theme.accent).opacity(0.3).blur(22.0)) - .padding(pill::inset()) - .align_child(Start, Center) - .on_hover(edge::over_bar) - .child(Stack::new(layers).width(row_w).height(pill::slot())); + let content = Rectangle::new().width(width).height(island_pill::height()).padding(pill::inset()).align_child(Start, Center).child(Stack::new(layers).width(row_w).height(pill::slot())); + + let pill = surface::shell(width, island_pill::height(), island_pill::height() / 2.0, &theme.surfaces(), 0.0, content).on_hover(edge::over_bar); // clicks next to the pills reach the windows underneath let mut regions = vec![InputArea { x: pill::left() as i32, y: island_pill::top_gap() as i32, width: width.ceil() as i32, height: island_pill::height() as i32 }];