feat: render theme-aware window borders
This commit is contained in:
1 parent
d6acfa89e7
commit
5bdb7bdf24
5 files changed
+354
-16
No files matched your search
@@ -45,6 +45,23 @@ them for a lone tiled window:
|
|||||||
smart_gaps = true
|
smart_gaps = true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Managed-window borders use theme tokens and overlay the window edge without
|
||||||
|
adding inner padding or changing client geometry:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[theme.colors]
|
||||||
|
border = "#FFFFFF18"
|
||||||
|
accent = "#5B8CFF"
|
||||||
|
|
||||||
|
[theme.geometry]
|
||||||
|
border_width = 1.0
|
||||||
|
focus_ring_width = 2.0
|
||||||
|
window_radius = 14.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Fullscreen windows are borderless. `window_radius` is validated now and is used
|
||||||
|
by the rounded-clipping pass described below in the M6 rendering work.
|
||||||
|
|
||||||
Scrolling keeps windows in stable workspace coordinates and animates the
|
Scrolling keeps windows in stable workspace coordinates and animates the
|
||||||
viewport only when focus must be revealed. Set
|
viewport only when focus must be revealed. Set
|
||||||
`focus_strategy = "center_on_focus"` under `[scrolling]` to center focused
|
`focus_strategy = "center_on_focus"` under `[scrolling]` to center focused
|
||||||
|
|||||||
+177
-1
@@ -23,6 +23,8 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
input: InputConfig,
|
input: InputConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
theme: ThemeConfig,
|
||||||
|
#[serde(default)]
|
||||||
commands: HashMap<String, Vec<String>>,
|
commands: HashMap<String, Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
bindings: Vec<BindingConfig>,
|
bindings: Vec<BindingConfig>,
|
||||||
@@ -32,6 +34,63 @@ pub struct Config {
|
|||||||
scrolling: ScrollingConfig,
|
scrolling: ScrollingConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct RgbaColor(pub [f32; 4]);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct ThemeSettings {
|
||||||
|
pub border_width: f64,
|
||||||
|
pub focus_ring_width: f64,
|
||||||
|
pub border_color: RgbaColor,
|
||||||
|
pub accent_color: RgbaColor,
|
||||||
|
pub window_radius: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
struct ThemeConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
colors: ThemeColorsConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
geometry: ThemeGeometryConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ThemeColorsConfig {
|
||||||
|
#[serde(default = "default_border_color")]
|
||||||
|
border: String,
|
||||||
|
#[serde(default = "default_accent_color")]
|
||||||
|
accent: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ThemeColorsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
border: default_border_color(),
|
||||||
|
accent: default_accent_color(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ThemeGeometryConfig {
|
||||||
|
#[serde(default = "default_border_width")]
|
||||||
|
border_width: f64,
|
||||||
|
#[serde(default = "default_focus_ring_width")]
|
||||||
|
focus_ring_width: f64,
|
||||||
|
#[serde(default = "default_window_radius")]
|
||||||
|
window_radius: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ThemeGeometryConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
border_width: default_border_width(),
|
||||||
|
focus_ring_width: default_focus_ring_width(),
|
||||||
|
window_radius: default_window_radius(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Binding {
|
pub struct Binding {
|
||||||
modifiers: BindingModifiers,
|
modifiers: BindingModifiers,
|
||||||
@@ -329,6 +388,10 @@ pub enum ConfigError {
|
|||||||
},
|
},
|
||||||
InvalidBinding(String),
|
InvalidBinding(String),
|
||||||
InvalidWindowRule(String),
|
InvalidWindowRule(String),
|
||||||
|
InvalidThemeValue {
|
||||||
|
field: &'static str,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ConfigError {
|
impl fmt::Display for ConfigError {
|
||||||
@@ -355,6 +418,9 @@ impl fmt::Display for ConfigError {
|
|||||||
}
|
}
|
||||||
Self::InvalidBinding(message) => write!(formatter, "invalid binding: {message}"),
|
Self::InvalidBinding(message) => write!(formatter, "invalid binding: {message}"),
|
||||||
Self::InvalidWindowRule(message) => write!(formatter, "invalid window rule: {message}"),
|
Self::InvalidWindowRule(message) => write!(formatter, "invalid window rule: {message}"),
|
||||||
|
Self::InvalidThemeValue { field, value } => {
|
||||||
|
write!(formatter, "invalid theme.{field} value {value}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +435,8 @@ impl Error for ConfigError {
|
|||||||
| Self::InvalidLayoutValue { .. }
|
| Self::InvalidLayoutValue { .. }
|
||||||
| Self::InvalidInputValue { .. }
|
| Self::InvalidInputValue { .. }
|
||||||
| Self::InvalidBinding(_)
|
| Self::InvalidBinding(_)
|
||||||
| Self::InvalidWindowRule(_) => None,
|
| Self::InvalidWindowRule(_)
|
||||||
|
| Self::InvalidThemeValue { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -520,6 +587,25 @@ impl Config {
|
|||||||
window_rules::validate(&self.window_rules).map_err(ConfigError::InvalidWindowRule)
|
window_rules::validate(&self.window_rules).map_err(ConfigError::InvalidWindowRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn theme_settings(&self) -> Result<ThemeSettings, ConfigError> {
|
||||||
|
let border_width =
|
||||||
|
nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?;
|
||||||
|
let focus_ring_width = nonnegative_theme_value(
|
||||||
|
self.theme.geometry.focus_ring_width,
|
||||||
|
"geometry.focus_ring_width",
|
||||||
|
)?;
|
||||||
|
let window_radius =
|
||||||
|
nonnegative_theme_value(self.theme.geometry.window_radius, "geometry.window_radius")?;
|
||||||
|
|
||||||
|
Ok(ThemeSettings {
|
||||||
|
border_width,
|
||||||
|
focus_ring_width,
|
||||||
|
border_color: parse_color(&self.theme.colors.border, "colors.border")?,
|
||||||
|
accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?,
|
||||||
|
window_radius,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn animations_enabled(&self) -> bool {
|
pub fn animations_enabled(&self) -> bool {
|
||||||
self.animations.enabled && !self.animations.reduced_motion
|
self.animations.enabled && !self.animations.reduced_motion
|
||||||
}
|
}
|
||||||
@@ -908,6 +994,54 @@ fn nonnegative_layout_value(value: f64, field: &'static str) -> Result<f64, Conf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn nonnegative_theme_value(value: f64, field: &'static str) -> Result<f64, ConfigError> {
|
||||||
|
if value.is_finite() && value >= 0.0 {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(ConfigError::InvalidThemeValue {
|
||||||
|
field,
|
||||||
|
value: value.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_color(value: &str, field: &'static str) -> Result<RgbaColor, ConfigError> {
|
||||||
|
let digits = value.strip_prefix('#').unwrap_or(value);
|
||||||
|
if !digits.is_ascii() || !matches!(digits.len(), 6 | 8) {
|
||||||
|
return Err(ConfigError::InvalidThemeValue {
|
||||||
|
field,
|
||||||
|
value: value.to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let parse_channel = |offset| u8::from_str_radix(&digits[offset..offset + 2], 16).ok();
|
||||||
|
let Some((red, green, blue)) = parse_channel(0)
|
||||||
|
.zip(parse_channel(2))
|
||||||
|
.zip(parse_channel(4))
|
||||||
|
.map(|((red, green), blue)| (red, green, blue))
|
||||||
|
else {
|
||||||
|
return Err(ConfigError::InvalidThemeValue {
|
||||||
|
field,
|
||||||
|
value: value.to_owned(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let alpha = if digits.len() == 8 {
|
||||||
|
parse_channel(6).ok_or_else(|| ConfigError::InvalidThemeValue {
|
||||||
|
field,
|
||||||
|
value: value.to_owned(),
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
u8::MAX
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(RgbaColor([
|
||||||
|
f32::from(red) / 255.0,
|
||||||
|
f32::from(green) / 255.0,
|
||||||
|
f32::from(blue) / 255.0,
|
||||||
|
f32::from(alpha) / 255.0,
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
const fn enabled_by_default() -> bool {
|
const fn enabled_by_default() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -920,6 +1054,26 @@ const fn default_outer_gap() -> f64 {
|
|||||||
10.0
|
10.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_border_color() -> String {
|
||||||
|
"#FFFFFF18".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_accent_color() -> String {
|
||||||
|
"#5B8CFF".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_border_width() -> f64 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_focus_ring_width() -> f64 {
|
||||||
|
2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_window_radius() -> f64 {
|
||||||
|
14.0
|
||||||
|
}
|
||||||
|
|
||||||
fn default_xkb_layout() -> String {
|
fn default_xkb_layout() -> String {
|
||||||
"us".to_owned()
|
"us".to_owned()
|
||||||
}
|
}
|
||||||
@@ -1108,6 +1262,28 @@ mod tests {
|
|||||||
assert!(invalid.gap_config().is_err());
|
assert!(invalid.gap_config().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_and_validates_theme_window_tokens() {
|
||||||
|
let configured = parse(
|
||||||
|
"[theme.colors]\nborder = \"#11223344\"\naccent = \"#AABBCC\"\n\n[theme.geometry]\nborder_width = 1.5\nfocus_ring_width = 3.0\nwindow_radius = 12.0",
|
||||||
|
);
|
||||||
|
let invalid_color = parse("[theme.colors]\naccent = \"blue\"");
|
||||||
|
let invalid_radius = parse("[theme.geometry]\nwindow_radius = -1.0");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
configured.theme_settings().unwrap(),
|
||||||
|
ThemeSettings {
|
||||||
|
border_width: 1.5,
|
||||||
|
focus_ring_width: 3.0,
|
||||||
|
border_color: RgbaColor([17.0 / 255.0, 34.0 / 255.0, 51.0 / 255.0, 68.0 / 255.0,]),
|
||||||
|
accent_color: RgbaColor([170.0 / 255.0, 187.0 / 255.0, 0.8, 1.0]),
|
||||||
|
window_radius: 12.0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(invalid_color.theme_settings().is_err());
|
||||||
|
assert!(invalid_radius.theme_settings().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_input_and_touchpad_settings() {
|
fn parses_input_and_touchpad_settings() {
|
||||||
let config = parse(
|
let config = parse(
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
input_settings,
|
input_settings,
|
||||||
bindings,
|
bindings,
|
||||||
window_rules,
|
window_rules,
|
||||||
|
theme_settings: config.theme_settings()?,
|
||||||
default_column_width: config.default_column_width()?,
|
default_column_width: config.default_column_width()?,
|
||||||
scrolling_focus_strategy: config.scrolling_focus_strategy(),
|
scrolling_focus_strategy: config.scrolling_focus_strategy(),
|
||||||
column_width_presets: config.width_presets()?,
|
column_width_presets: config.width_presets()?,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ use smithay::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Binding, InputSettings},
|
config::{Binding, InputSettings, ThemeSettings},
|
||||||
window_rules::{WindowRule, resolve as resolve_window_rules},
|
window_rules::{WindowRule, resolve as resolve_window_rules},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,6 +92,7 @@ pub struct Ferese {
|
|||||||
output_identity_ids: HashMap<String, OutputId>,
|
output_identity_ids: HashMap<String, OutputId>,
|
||||||
pub window_ids: HashMap<Window, WindowId>,
|
pub window_ids: HashMap<Window, WindowId>,
|
||||||
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
||||||
|
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
|
||||||
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
|
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
|
||||||
scrolling_world_x: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
scrolling_world_x: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
||||||
pub focused_window: Option<WindowId>,
|
pub focused_window: Option<WindowId>,
|
||||||
@@ -101,6 +102,7 @@ pub struct Ferese {
|
|||||||
pub(crate) bindings: Vec<Binding>,
|
pub(crate) bindings: Vec<Binding>,
|
||||||
window_rules: Vec<WindowRule>,
|
window_rules: Vec<WindowRule>,
|
||||||
window_rules_applied: HashSet<WindowId>,
|
window_rules_applied: HashSet<WindowId>,
|
||||||
|
pub(crate) theme_settings: ThemeSettings,
|
||||||
animations_enabled: bool,
|
animations_enabled: bool,
|
||||||
animation_speed: f64,
|
animation_speed: f64,
|
||||||
spring_config: SpringConfig,
|
spring_config: SpringConfig,
|
||||||
@@ -153,6 +155,7 @@ pub struct RuntimeConfig {
|
|||||||
pub input_settings: InputSettings,
|
pub input_settings: InputSettings,
|
||||||
pub bindings: Vec<Binding>,
|
pub bindings: Vec<Binding>,
|
||||||
pub window_rules: Vec<WindowRule>,
|
pub window_rules: Vec<WindowRule>,
|
||||||
|
pub theme_settings: ThemeSettings,
|
||||||
pub default_column_width: ColumnWidth,
|
pub default_column_width: ColumnWidth,
|
||||||
pub scrolling_focus_strategy: ViewportFocusStrategy,
|
pub scrolling_focus_strategy: ViewportFocusStrategy,
|
||||||
pub column_width_presets: Vec<ColumnWidth>,
|
pub column_width_presets: Vec<ColumnWidth>,
|
||||||
@@ -390,6 +393,7 @@ impl Ferese {
|
|||||||
output_identity_ids: HashMap::new(),
|
output_identity_ids: HashMap::new(),
|
||||||
window_ids: HashMap::new(),
|
window_ids: HashMap::new(),
|
||||||
window_geometry: HashMap::new(),
|
window_geometry: HashMap::new(),
|
||||||
|
window_borders: HashMap::new(),
|
||||||
viewport_animations: HashMap::new(),
|
viewport_animations: HashMap::new(),
|
||||||
scrolling_world_x: HashMap::new(),
|
scrolling_world_x: HashMap::new(),
|
||||||
focused_window: None,
|
focused_window: None,
|
||||||
@@ -399,6 +403,7 @@ impl Ferese {
|
|||||||
bindings: config.bindings,
|
bindings: config.bindings,
|
||||||
window_rules: config.window_rules,
|
window_rules: config.window_rules,
|
||||||
window_rules_applied: HashSet::new(),
|
window_rules_applied: HashSet::new(),
|
||||||
|
theme_settings: config.theme_settings,
|
||||||
animations_enabled: config.animations_enabled,
|
animations_enabled: config.animations_enabled,
|
||||||
animation_speed: config.animation_speed,
|
animation_speed: config.animation_speed,
|
||||||
spring_config: config.spring_config,
|
spring_config: config.spring_config,
|
||||||
@@ -772,6 +777,7 @@ impl Ferese {
|
|||||||
|
|
||||||
self.space.unmap_elem(window);
|
self.space.unmap_elem(window);
|
||||||
self.window_geometry.remove(&id);
|
self.window_geometry.remove(&id);
|
||||||
|
self.window_borders.remove(&id);
|
||||||
self.window_rules_applied.remove(&id);
|
self.window_rules_applied.remove(&id);
|
||||||
self.scrolling_world_x.remove(&id);
|
self.scrolling_world_x.remove(&id);
|
||||||
if let Err(error) = self.workspaces.remove_window(id) {
|
if let Err(error) = self.workspaces.remove_window(id) {
|
||||||
|
|||||||
+152
-14
@@ -12,6 +12,7 @@ use smithay::{
|
|||||||
AsRenderElements, Kind as RenderElementKind, RenderElement,
|
AsRenderElements, Kind as RenderElementKind, RenderElement,
|
||||||
memory::MemoryRenderBufferRenderElement,
|
memory::MemoryRenderBufferRenderElement,
|
||||||
render_elements,
|
render_elements,
|
||||||
|
solid::{SolidColorBuffer, SolidColorRenderElement},
|
||||||
surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree},
|
surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree},
|
||||||
utils::{
|
utils::{
|
||||||
ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate,
|
ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate,
|
||||||
@@ -51,6 +52,20 @@ render_elements! {
|
|||||||
pub(crate) AnimatedWindowRenderElement<=GlesRenderer>;
|
pub(crate) AnimatedWindowRenderElement<=GlesRenderer>;
|
||||||
Surface=SurfaceRenderElement,
|
Surface=SurfaceRenderElement,
|
||||||
Memory=MemoryRenderElement,
|
Memory=MemoryRenderElement,
|
||||||
|
Solid=SolidColorRenderElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct WindowBorderBuffers {
|
||||||
|
edges: [SolidColorBuffer; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WindowBorderBuffers {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
edges: std::array::from_fn(|_| SolidColorBuffer::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<(), Box<dyn Error>> {
|
pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<(), Box<dyn Error>> {
|
||||||
@@ -216,7 +231,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn animated_window_elements(
|
pub(crate) fn animated_window_elements(
|
||||||
state: &Ferese,
|
state: &mut Ferese,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
@@ -224,7 +239,7 @@ pub(crate) fn animated_window_elements(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cursorless_window_elements(
|
pub(crate) fn cursorless_window_elements(
|
||||||
state: &Ferese,
|
state: &mut Ferese,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
@@ -232,7 +247,7 @@ pub(crate) fn cursorless_window_elements(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn output_elements(
|
fn output_elements(
|
||||||
state: &Ferese,
|
state: &mut Ferese,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
include_cursor: bool,
|
include_cursor: bool,
|
||||||
@@ -252,14 +267,24 @@ fn output_elements(
|
|||||||
output,
|
output,
|
||||||
&[Layer::Overlay, Layer::Top],
|
&[Layer::Overlay, Layer::Top],
|
||||||
));
|
));
|
||||||
elements.extend(
|
let windows = state
|
||||||
state
|
|
||||||
.space
|
.space
|
||||||
.elements()
|
.elements()
|
||||||
.rev()
|
.rev()
|
||||||
.filter_map(|window| {
|
.filter_map(|window| {
|
||||||
let id = state.window_ids.get(window)?;
|
let id = *state.window_ids.get(window)?;
|
||||||
let visual = state.window_geometry.get(id)?.visual.current;
|
let visual = state.window_geometry.get(&id)?.visual.current;
|
||||||
|
let fullscreen = state
|
||||||
|
.workspaces
|
||||||
|
.workspace_for_window(id)
|
||||||
|
.and_then(|workspace| state.workspaces.workspace(workspace))
|
||||||
|
.is_some_and(|workspace| workspace.fullscreen == Some(id));
|
||||||
|
|
||||||
|
Some((window.clone(), id, visual, fullscreen))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (window, id, visual, fullscreen) in windows {
|
||||||
let constrain = Rectangle::<i32, Logical>::new(
|
let constrain = Rectangle::<i32, Logical>::new(
|
||||||
(
|
(
|
||||||
(visual.x - f64::from(output_geometry.loc.x)).round() as i32,
|
(visual.x - f64::from(output_geometry.loc.x)).round() as i32,
|
||||||
@@ -273,13 +298,16 @@ fn output_elements(
|
|||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Some(constrain_space_element::<
|
if !fullscreen {
|
||||||
|
elements.extend(window_border_elements(state, id, constrain, scale));
|
||||||
|
}
|
||||||
|
elements.extend(constrain_space_element::<
|
||||||
GlesRenderer,
|
GlesRenderer,
|
||||||
_,
|
_,
|
||||||
AnimatedWindowRenderElement,
|
AnimatedWindowRenderElement,
|
||||||
>(
|
>(
|
||||||
renderer,
|
renderer,
|
||||||
window,
|
&window,
|
||||||
constrain.loc,
|
constrain.loc,
|
||||||
1.0,
|
1.0,
|
||||||
scale,
|
scale,
|
||||||
@@ -289,10 +317,8 @@ fn output_elements(
|
|||||||
behavior: ConstrainScaleBehavior::Stretch,
|
behavior: ConstrainScaleBehavior::Stretch,
|
||||||
align: ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
align: ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
||||||
},
|
},
|
||||||
))
|
));
|
||||||
})
|
}
|
||||||
.flatten(),
|
|
||||||
);
|
|
||||||
elements.extend(layer_elements(
|
elements.extend(layer_elements(
|
||||||
renderer,
|
renderer,
|
||||||
output,
|
output,
|
||||||
@@ -301,6 +327,88 @@ fn output_elements(
|
|||||||
elements
|
elements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn window_border_elements(
|
||||||
|
state: &mut Ferese,
|
||||||
|
id: ferese_layout::WindowId,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
|
let focused = state.focused_window == Some(id);
|
||||||
|
let width = if focused {
|
||||||
|
state.theme_settings.focus_ring_width
|
||||||
|
} else {
|
||||||
|
state.theme_settings.border_width
|
||||||
|
};
|
||||||
|
let rectangles = border_rectangles(geometry, width);
|
||||||
|
if rectangles.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = if focused {
|
||||||
|
state.theme_settings.accent_color.0
|
||||||
|
} else {
|
||||||
|
state.theme_settings.border_color.0
|
||||||
|
};
|
||||||
|
let buffers = state.window_borders.entry(id).or_default();
|
||||||
|
|
||||||
|
buffers
|
||||||
|
.edges
|
||||||
|
.iter_mut()
|
||||||
|
.zip(rectangles)
|
||||||
|
.map(|(buffer, rectangle)| {
|
||||||
|
buffer.update(rectangle.size, color);
|
||||||
|
SolidColorRenderElement::from_buffer(
|
||||||
|
buffer,
|
||||||
|
rectangle.loc.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
1.0,
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn border_rectangles(
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
requested_width: f64,
|
||||||
|
) -> Vec<Rectangle<i32, Logical>> {
|
||||||
|
let width = requested_width
|
||||||
|
.ceil()
|
||||||
|
.max(0.0)
|
||||||
|
.min(f64::from(geometry.size.w.min(geometry.size.h)) / 2.0) as i32;
|
||||||
|
if width == 0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let inner_height = geometry.size.h - width * 2;
|
||||||
|
let mut rectangles = vec![
|
||||||
|
Rectangle::new(geometry.loc, (geometry.size.w, width).into()),
|
||||||
|
Rectangle::new(
|
||||||
|
(geometry.loc.x, geometry.loc.y + geometry.size.h - width).into(),
|
||||||
|
(geometry.size.w, width).into(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if inner_height > 0 {
|
||||||
|
rectangles.extend([
|
||||||
|
Rectangle::new(
|
||||||
|
(geometry.loc.x, geometry.loc.y + width).into(),
|
||||||
|
(width, inner_height).into(),
|
||||||
|
),
|
||||||
|
Rectangle::new(
|
||||||
|
(
|
||||||
|
geometry.loc.x + geometry.size.w - width,
|
||||||
|
geometry.loc.y + width,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
(width, inner_height).into(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
rectangles
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn redraw_output<R, E>(
|
pub(crate) fn redraw_output<R, E>(
|
||||||
renderer: &mut R,
|
renderer: &mut R,
|
||||||
framebuffer: &mut R::Framebuffer<'_>,
|
framebuffer: &mut R::Framebuffer<'_>,
|
||||||
@@ -465,7 +573,9 @@ fn normalized_scale(scale: f64) -> f64 {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::normalized_scale;
|
use smithay::utils::{Logical, Rectangle};
|
||||||
|
|
||||||
|
use super::{border_rectangles, normalized_scale};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn accepts_positive_finite_scale() {
|
fn accepts_positive_finite_scale() {
|
||||||
@@ -478,4 +588,32 @@ mod tests {
|
|||||||
assert_eq!(normalized_scale(f64::NAN), 1.0);
|
assert_eq!(normalized_scale(f64::NAN), 1.0);
|
||||||
assert_eq!(normalized_scale(f64::INFINITY), 1.0);
|
assert_eq!(normalized_scale(f64::INFINITY), 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn border_rectangles_stay_inside_window_geometry() {
|
||||||
|
let geometry = Rectangle::<i32, Logical>::new((10, 20).into(), (100, 80).into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
border_rectangles(geometry, 2.0),
|
||||||
|
vec![
|
||||||
|
Rectangle::new((10, 20).into(), (100, 2).into()),
|
||||||
|
Rectangle::new((10, 98).into(), (100, 2).into()),
|
||||||
|
Rectangle::new((10, 22).into(), (2, 76).into()),
|
||||||
|
Rectangle::new((108, 22).into(), (2, 76).into()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn border_width_is_clamped_for_tiny_windows() {
|
||||||
|
let geometry = Rectangle::<i32, Logical>::new((0, 0).into(), (3, 2).into());
|
||||||
|
let rectangles = border_rectangles(geometry, 20.0);
|
||||||
|
|
||||||
|
assert_eq!(rectangles.len(), 2);
|
||||||
|
assert!(
|
||||||
|
rectangles
|
||||||
|
.iter()
|
||||||
|
.all(|rectangle| geometry.contains_rect(*rectangle))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user