Commit 7ce6ae4e5a85
tarball5 files changed+129-8
@@ -13,6 +13,12 @@ gaps = { inner = 8, outer = 8 }
1313
# merge joins its lane. invert-move does the other one for the next move.1414
move = "swap"1515
16
+[decoration]17
+# Borders sit in the gap around each window, so keep them under half the inner gap.18
+border = 219
+focused = "#88bfd1"20
+unfocused = "#3b4252"21
+1622
[animation]1723
enabled = true1824
# orthogonal moves each window along one axis per phase, direct allows both.@@ -12,6 +12,7 @@ pub const DEFAULT: &str = include_str!("../default.toml");
1212
#[derive(Debug, Clone)]1313
pub struct Config {1414
pub general: General,15
+ pub decoration: Decoration,1516
pub animation: Animation,1617
pub keyboard: Keyboard,1718
pub binds: Vec<Bind>,@@ -69,6 +70,47 @@ impl Default for Gaps {
6970
7071
#[derive(Deserialize, Debug, Clone, PartialEq)]7172
#[serde(default, deny_unknown_fields)]73
+pub struct Decoration {74
+ /// Border width in logical pixels, drawn in the gap around each window.75
+ pub border: u32,76
+ pub focused: Color,77
+ pub unfocused: Color,78
+}79
+80
+impl Default for Decoration {81
+ fn default() -> Self {82
+ Self {83
+ border: 2,84
+ focused: Color([0.53, 0.75, 0.82, 1.0]),85
+ unfocused: Color([0.23, 0.26, 0.32, 1.0]),86
+ }87
+ }88
+}89
+90
+/// Straight RGBA from `#rrggbb` or `#rrggbbaa`.91
+#[derive(Debug, Clone, Copy, PartialEq)]92
+pub struct Color(pub [f32; 4]);93
+94
+impl<'de> Deserialize<'de> for Color {95
+ fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {96
+ let s = String::deserialize(d)?;97
+ let hex = s.strip_prefix('#').unwrap_or(&s);98
+ let bad =99
+ || serde::de::Error::custom(format!("bad colour {s:?}, use #rrggbb or #rrggbbaa"));100
+ if !matches!(hex.len(), 6 | 8) || !hex.is_ascii() {101
+ return Err(bad());102
+ }103
+ let mut c = [1.0f32; 4];104
+ for (i, v) in c.iter_mut().enumerate().take(hex.len() / 2) {105
+ let byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| bad())?;106
+ *v = f32::from(byte) / 255.0;107
+ }108
+ Ok(Self(c))109
+ }110
+}111
+112
+#[derive(Deserialize, Debug, Clone, PartialEq)]113
+#[serde(default, deny_unknown_fields)]72114
pub struct Animation {73115
pub enabled: bool,74116
pub style: AnimationStyle,@@ -212,6 +254,8 @@ struct Raw {
212254
#[serde(default)]213255
animation: Animation,214256
#[serde(default)]257
+ decoration: Decoration,258
+ #[serde(default)]215259
binds: BTreeMap<String, Words>,216260
#[serde(default)]217261
rules: Vec<RawRule>,@@ -290,6 +334,7 @@ impl Config {
290334
Ok(Self {291335
general: raw.general,292336
animation: raw.animation,337
+ decoration: raw.decoration,293338
keyboard: raw.keyboard,294339
binds,295340
rules,@@ -176,6 +176,8 @@ pub fn render(state: &mut State) {
176176
layout,177177
windows,178178
anim,179
+ borders,180
+ config,179181
..180182
} = state;181183
let Some(host) = host.as_mut() else { return };@@ -219,7 +221,15 @@ pub fn render(state: &mut State) {
219221
return;220222
}221223
};222
- let elements = crate::scene::build(layout, windows, anim, renderer, now);224
+ let elements = crate::scene::build(225
+ layout,226
+ windows,227
+ anim,228
+ renderer,229
+ now,230
+ &config.decoration,231
+ borders,232
+ );223233
match host224234
.damage225235
.render_output(renderer, &mut fb, slot.age() as usize, &elements, CLEAR)@@ -2,8 +2,10 @@
22
//! scene draws windows where the animator says they are right now.33
44
use crate::anim::Animator;5
+use monolith_config::Decoration;56
use monolith_layout::{Layout, Rect, WindowId};67
use monolith_render_vk::{Effect, VulkanFrame, VulkanRenderer, VulkanTexture};8
+use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};79
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;810
use smithay::backend::renderer::element::texture::TextureRenderElement;911
use smithay::backend::renderer::element::utils::CropRenderElement;@@ -22,8 +24,14 @@ smithay::backend::renderer::element::render_elements! {
2224
Cropped=CropRenderElement<WaylandSurfaceRenderElement<VulkanRenderer>>,2325
Texture=TextureRenderElement<VulkanTexture>,2426
Effect=EffectElement,27
+ Border=SolidColorRenderElement,2528
}2629
30
+/// Border strips per window, kept between frames so unchanged borders cause31
+/// no damage.32
+#[derive(Default)]33
+pub struct Borders(HashMap<WindowId, [SolidColorBuffer; 4]>);34
+2735
/// Elements for every output, front to back.2836
pub fn build(2937
layout: &Layout,@@ -31,6 +39,8 @@ pub fn build(
3139
anim: &Animator,3240
renderer: &mut VulkanRenderer,3341
now: u64,42
+ deco: &Decoration,43
+ borders: &mut Borders,3444
) -> Vec<Draw> {3545
let mut ghosts = Vec::new();3646
let mut tiled = Vec::new();@@ -50,24 +60,37 @@ pub fn build(
5060
continue;5161
}5262
match anim.gliding(p.window, now) {53
- Some(r) => above.push((p.window, r)),63
+ Some(r) => above.push((p.window, r, true)),5464
None => {5565
let r = anim.tiled(o, p.window, now).unwrap_or(p.rect);56
- tiled.push((p.window, r));66
+ tiled.push((p.window, r, true));5767
}5868
}5969
}6070
for &(w, r) in &arr.floating {61
- above.push((w, anim.gliding(w, now).unwrap_or(r)));71
+ above.push((w, anim.gliding(w, now).unwrap_or(r), true));6272
}6373
if let Some(w) = arr.fullscreen {64
- above.push((w, anim.gliding(w, now).unwrap_or(out.area)));74
+ above.push((w, anim.gliding(w, now).unwrap_or(out.area), false));6575
}6676
}77
+ borders.0.retain(|id, _| windows.contains_key(id));78
+ let focused = layout.focused();6779
let mut out = Vec::new();68
- for (id, r) in tiled.into_iter().chain(above).rev() {69
- if let Some(window) = windows.get(&id) {70
- window_elements(&mut out, renderer, window, r, anim.effect(id, now));80
+ for (id, r, framed) in tiled.into_iter().chain(above).rev() {81
+ let Some(window) = windows.get(&id) else {82
+ continue;83
+ };84
+ let effect = anim.effect(id, now);85
+ window_elements(&mut out, renderer, window, r, effect);86
+ if framed && deco.border > 0 {87
+ let color = if focused == Some(id) {88
+ deco.focused.089
+ } else {90
+ deco.unfocused.091
+ };92
+ let alpha = effect.map_or(1.0, |e| e.progress);93
+ border_elements(&mut out, borders, id, r, deco.border as i32, color, alpha);7194
}7295
}7396
for (g, r) in ghosts.into_iter().rev() {@@ -163,6 +186,41 @@ fn window_elements(
163186
);164187
}165188
189
+/// Four strips just outside `r`, in the gap, so the client keeps its size.190
+fn border_elements(191
+ out: &mut Vec<Draw>,192
+ borders: &mut Borders,193
+ id: WindowId,194
+ r: Rect,195
+ b: i32,196
+ color: [f32; 4],197
+ alpha: f32,198
+) {199
+ let Some(((x, y), (w, h))) = pixels(r) else {200
+ return;201
+ };202
+ let strips = [203
+ ((x - b, y - b), (w + 2 * b, b)),204
+ ((x - b, y + h), (w + 2 * b, b)),205
+ ((x - b, y), (b, h)),206
+ ((x + w, y), (b, h)),207
+ ];208
+ let buffers = borders209
+ .0210
+ .entry(id)211
+ .or_insert_with(|| std::array::from_fn(|_| SolidColorBuffer::new((0, 0), color)));212
+ for (buffer, (at, size)) in buffers.iter_mut().zip(strips) {213
+ buffer.update(size, color);214
+ out.push(Draw::Border(SolidColorRenderElement::from_buffer(215
+ buffer,216
+ at,217
+ 1.0,218
+ alpha,219
+ Kind::Unspecified,220
+ )));221
+ }222
+}223
+166224
/// Snaps a logical rect to whole pixels by rounding its edges, so neighbours167225
/// that share an edge stay flush.168226
fn pixels(r: Rect) -> Option<((i32, i32), (i32, i32))> {@@ -62,6 +62,7 @@ pub struct State {
6262
pub launches: crate::launch::Launches,6363
pub workspaces: WorkspaceState,6464
pub anim: Animator,65
+ pub borders: crate::scene::Borders,6566
/// How each shown window was placed by the last layout pass, to spot6667
/// switches between tiled, floating and fullscreen.6768
pub last_kind: HashMap<WindowId, (Kind, Rect, OutputId)>,@@ -147,6 +148,7 @@ impl State {
147148
launches: Default::default(),148149
workspaces: WorkspaceState::new::<State>(&dh),149150
anim: Animator::new(animation),151
+ borders: Default::default(),150152
last_kind: HashMap::new(),151153
compositor_state: CompositorState::new::<State>(&dh),152154
xdg_shell_state: XdgShellState::new::<State>(&dh),