tiling: Add code to render group hints
This commit is contained in:
parent
84b3213146
commit
4ea0136a9b
8 changed files with 739 additions and 276 deletions
|
|
@ -1,2 +1,2 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.65"
|
channel = "1.66"
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::{Borrow, BorrowMut},
|
borrow::{Borrow, BorrowMut},
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
|
collections::HashMap,
|
||||||
|
sync::Weak,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
|
|
@ -12,8 +14,8 @@ use crate::{
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
shell::{
|
shell::{
|
||||||
element::window::CosmicWindowRenderElement, layout::floating::SeatMoveGrabState,
|
element::window::CosmicWindowRenderElement, focus::target::WindowGroup,
|
||||||
CosmicMappedRenderElement,
|
layout::floating::SeatMoveGrabState, CosmicMapped, CosmicMappedRenderElement,
|
||||||
},
|
},
|
||||||
state::{Common, Fps},
|
state::{Common, Fps},
|
||||||
utils::prelude::SeatExt,
|
utils::prelude::SeatExt,
|
||||||
|
|
@ -47,7 +49,7 @@ use smithay::{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output::Output,
|
output::Output,
|
||||||
utils::{Logical, Physical, Point, Rectangle, Size},
|
utils::{IsAlive, Logical, Physical, Point, Rectangle, Size},
|
||||||
wayland::{
|
wayland::{
|
||||||
dmabuf::get_dmabuf,
|
dmabuf::get_dmabuf,
|
||||||
shm::{shm_format_to_fourcc, with_buffer_contents},
|
shm::{shm_format_to_fourcc, with_buffer_contents},
|
||||||
|
|
@ -66,11 +68,54 @@ pub type GlMultiFrame<'a, 'b, 'frame> =
|
||||||
MultiFrame<'a, 'a, 'b, 'frame, GbmGlesBackend<GlowRenderer>, GbmGlesBackend<GlowRenderer>>;
|
MultiFrame<'a, 'a, 'b, 'frame, GbmGlesBackend<GlowRenderer>, GbmGlesBackend<GlowRenderer>>;
|
||||||
|
|
||||||
pub static CLEAR_COLOR: [f32; 4] = [0.153, 0.161, 0.165, 1.0];
|
pub static CLEAR_COLOR: [f32; 4] = [0.153, 0.161, 0.165, 1.0];
|
||||||
|
pub static ACTIVE_GROUP_COLOR: [f32; 3] = [0.678, 0.635, 0.619];
|
||||||
|
pub static GROUP_COLOR: [f32; 3] = [0.431, 0.404, 0.396];
|
||||||
pub static FOCUS_INDICATOR_COLOR: [f32; 3] = [0.580, 0.921, 0.921];
|
pub static FOCUS_INDICATOR_COLOR: [f32; 3] = [0.580, 0.921, 0.921];
|
||||||
pub static FOCUS_INDICATOR_SHADER: &str = include_str!("./shaders/focus_indicator.frag");
|
pub static FOCUS_INDICATOR_SHADER: &str = include_str!("./shaders/focus_indicator.frag");
|
||||||
|
|
||||||
pub struct IndicatorShader(pub GlesPixelProgram);
|
pub struct IndicatorShader(pub GlesPixelProgram);
|
||||||
struct IndicatorElement(pub RefCell<PixelShaderElement>);
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Key {
|
||||||
|
Group(Weak<()>),
|
||||||
|
Window(CosmicMapped),
|
||||||
|
}
|
||||||
|
impl std::hash::Hash for Key {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
match self {
|
||||||
|
Key::Group(arc) => (arc.as_ptr() as usize).hash(state),
|
||||||
|
Key::Window(window) => window.hash(state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl PartialEq for Key {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Key::Group(g1), Key::Group(g2)) => Weak::ptr_eq(g1, g2),
|
||||||
|
(Key::Window(w1), Key::Window(w2)) => w1 == w2,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Eq for Key {}
|
||||||
|
impl From<CosmicMapped> for Key {
|
||||||
|
fn from(window: CosmicMapped) -> Self {
|
||||||
|
Key::Window(window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<WindowGroup> for Key {
|
||||||
|
fn from(group: WindowGroup) -> Self {
|
||||||
|
Key::Group(group.alive.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct IndicatorSettings {
|
||||||
|
thickness: u8,
|
||||||
|
alpha: f32,
|
||||||
|
color: [f32; 3],
|
||||||
|
}
|
||||||
|
type IndicatorCache = RefCell<HashMap<Key, (IndicatorSettings, PixelShaderElement)>>;
|
||||||
|
|
||||||
impl IndicatorShader {
|
impl IndicatorShader {
|
||||||
pub fn get<R: AsGlowRenderer>(renderer: &R) -> GlesPixelProgram {
|
pub fn get<R: AsGlowRenderer>(renderer: &R) -> GlesPixelProgram {
|
||||||
|
|
@ -85,50 +130,63 @@ impl IndicatorShader {
|
||||||
|
|
||||||
pub fn element<R: AsGlowRenderer>(
|
pub fn element<R: AsGlowRenderer>(
|
||||||
renderer: &R,
|
renderer: &R,
|
||||||
|
key: impl Into<Key>,
|
||||||
geo: Rectangle<i32, Logical>,
|
geo: Rectangle<i32, Logical>,
|
||||||
thickness: u8,
|
thickness: u8,
|
||||||
alpha: f32,
|
alpha: f32,
|
||||||
|
color: [f32; 3],
|
||||||
) -> PixelShaderElement {
|
) -> PixelShaderElement {
|
||||||
let thickness: f32 = thickness as f32;
|
let settings = IndicatorSettings {
|
||||||
let thickness_loc = (thickness as i32, thickness as i32);
|
thickness,
|
||||||
let thickness_size = ((thickness * 2.0) as i32, (thickness * 2.0) as i32);
|
alpha,
|
||||||
let geo = Rectangle::from_loc_and_size(
|
color,
|
||||||
geo.loc - Point::from(thickness_loc),
|
};
|
||||||
geo.size + Size::from(thickness_size),
|
|
||||||
);
|
|
||||||
|
|
||||||
let user_data = Borrow::<GlesRenderer>::borrow(renderer.glow_renderer())
|
let user_data = Borrow::<GlesRenderer>::borrow(renderer.glow_renderer())
|
||||||
.egl_context()
|
.egl_context()
|
||||||
.user_data();
|
.user_data();
|
||||||
|
|
||||||
match user_data.get::<IndicatorElement>() {
|
user_data.insert_if_missing(|| IndicatorCache::new(HashMap::new()));
|
||||||
Some(elem) => {
|
let mut cache = user_data.get::<IndicatorCache>().unwrap().borrow_mut();
|
||||||
let mut elem = elem.0.borrow_mut();
|
cache.retain(|k, _| match k {
|
||||||
if elem.geometry(1.0.into()).to_logical(1) != geo {
|
Key::Group(w) => w.upgrade().is_some(),
|
||||||
elem.resize(geo, None);
|
Key::Window(w) => w.alive(),
|
||||||
}
|
});
|
||||||
elem.clone()
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let shader = Self::get(renderer);
|
|
||||||
|
|
||||||
let elem = PixelShaderElement::new(
|
let key = key.into();
|
||||||
shader,
|
if cache
|
||||||
geo,
|
.get(&key)
|
||||||
None, //TODO
|
.filter(|(old_settings, _)| &settings == old_settings)
|
||||||
alpha,
|
.is_none()
|
||||||
vec![
|
{
|
||||||
Uniform::new("color", FOCUS_INDICATOR_COLOR),
|
let thickness: f32 = thickness as f32;
|
||||||
Uniform::new("thickness", thickness),
|
let thickness_loc = (thickness as i32, thickness as i32);
|
||||||
Uniform::new("radius", thickness * 2.0),
|
let thickness_size = ((thickness * 2.0) as i32, (thickness * 2.0) as i32);
|
||||||
],
|
let geo = Rectangle::from_loc_and_size(
|
||||||
);
|
geo.loc - Point::from(thickness_loc),
|
||||||
if !user_data.insert_if_missing(|| IndicatorElement(RefCell::new(elem.clone()))) {
|
geo.size + Size::from(thickness_size),
|
||||||
*user_data.get::<IndicatorElement>().unwrap().0.borrow_mut() = elem.clone();
|
);
|
||||||
}
|
let shader = Self::get(renderer);
|
||||||
elem
|
|
||||||
}
|
let elem = PixelShaderElement::new(
|
||||||
|
shader,
|
||||||
|
geo,
|
||||||
|
None, //TODO
|
||||||
|
alpha,
|
||||||
|
vec![
|
||||||
|
Uniform::new("color", color),
|
||||||
|
Uniform::new("thickness", thickness),
|
||||||
|
Uniform::new("radius", thickness * 2.0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
cache.insert(key.clone(), (settings, elem));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let elem = &mut cache.get_mut(&key).unwrap().1;
|
||||||
|
if elem.geometry(1.0.into()).to_logical(1) != geo {
|
||||||
|
elem.resize(geo, None);
|
||||||
|
}
|
||||||
|
elem.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -307,6 +365,7 @@ where
|
||||||
&state.shell.override_redirect_windows,
|
&state.shell.override_redirect_windows,
|
||||||
state.xwayland_state.as_mut(),
|
state.xwayland_state.as_mut(),
|
||||||
(!move_active && is_active_space).then_some(&last_active_seat),
|
(!move_active && is_active_space).then_some(&last_active_seat),
|
||||||
|
true,
|
||||||
state.config.static_conf.active_hint,
|
state.config.static_conf.active_hint,
|
||||||
exclude_workspace_overview,
|
exclude_workspace_overview,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ use smithay::{
|
||||||
input::KeyState,
|
input::KeyState,
|
||||||
renderer::{
|
renderer::{
|
||||||
element::{
|
element::{
|
||||||
utils::CropRenderElement, AsRenderElements, Element, RenderElement,
|
utils::{CropRenderElement, RelocateRenderElement, RescaleRenderElement},
|
||||||
UnderlyingStorage,
|
AsRenderElements, Element, RenderElement, UnderlyingStorage,
|
||||||
},
|
},
|
||||||
gles::element::PixelShaderElement,
|
gles::element::PixelShaderElement,
|
||||||
glow::GlowRenderer,
|
glow::GlowRenderer,
|
||||||
|
|
@ -259,7 +259,7 @@ impl CosmicMapped {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_tiled(&self, tiled: bool) {
|
pub fn set_tiled(&self, tiled: bool) {
|
||||||
for window in match &self.element {
|
if let Some(window) = match &self.element {
|
||||||
// we use the tiled state of stack windows anyway to get rid of decorations
|
// we use the tiled state of stack windows anyway to get rid of decorations
|
||||||
CosmicMappedInternal::Stack(_) => None,
|
CosmicMappedInternal::Stack(_) => None,
|
||||||
CosmicMappedInternal::Window(w) => Some(w.surface()),
|
CosmicMappedInternal::Window(w) => Some(w.surface()),
|
||||||
|
|
@ -681,8 +681,16 @@ where
|
||||||
{
|
{
|
||||||
Stack(self::stack::CosmicStackRenderElement<R>),
|
Stack(self::stack::CosmicStackRenderElement<R>),
|
||||||
Window(self::window::CosmicWindowRenderElement<R>),
|
Window(self::window::CosmicWindowRenderElement<R>),
|
||||||
CroppedStack(CropRenderElement<self::stack::CosmicStackRenderElement<R>>),
|
TiledStack(
|
||||||
CroppedWindow(CropRenderElement<self::window::CosmicWindowRenderElement<R>>),
|
RelocateRenderElement<
|
||||||
|
RescaleRenderElement<CropRenderElement<self::stack::CosmicStackRenderElement<R>>>,
|
||||||
|
>,
|
||||||
|
),
|
||||||
|
TiledWindow(
|
||||||
|
RelocateRenderElement<
|
||||||
|
RescaleRenderElement<CropRenderElement<self::window::CosmicWindowRenderElement<R>>>,
|
||||||
|
>,
|
||||||
|
),
|
||||||
Indicator(PixelShaderElement),
|
Indicator(PixelShaderElement),
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
Egui(TextureRenderElement<GlesTexture>),
|
Egui(TextureRenderElement<GlesTexture>),
|
||||||
|
|
@ -697,8 +705,8 @@ where
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.id(),
|
CosmicMappedRenderElement::Stack(elem) => elem.id(),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.id(),
|
CosmicMappedRenderElement::Window(elem) => elem.id(),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.id(),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.id(),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.id(),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.id(),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => elem.id(),
|
CosmicMappedRenderElement::Indicator(elem) => elem.id(),
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
CosmicMappedRenderElement::Egui(elem) => elem.id(),
|
CosmicMappedRenderElement::Egui(elem) => elem.id(),
|
||||||
|
|
@ -709,8 +717,8 @@ where
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.current_commit(),
|
CosmicMappedRenderElement::Stack(elem) => elem.current_commit(),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.current_commit(),
|
CosmicMappedRenderElement::Window(elem) => elem.current_commit(),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.current_commit(),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.current_commit(),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.current_commit(),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.current_commit(),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => elem.current_commit(),
|
CosmicMappedRenderElement::Indicator(elem) => elem.current_commit(),
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
CosmicMappedRenderElement::Egui(elem) => elem.current_commit(),
|
CosmicMappedRenderElement::Egui(elem) => elem.current_commit(),
|
||||||
|
|
@ -721,8 +729,8 @@ where
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.src(),
|
CosmicMappedRenderElement::Stack(elem) => elem.src(),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.src(),
|
CosmicMappedRenderElement::Window(elem) => elem.src(),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.src(),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.src(),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.src(),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.src(),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => elem.src(),
|
CosmicMappedRenderElement::Indicator(elem) => elem.src(),
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
CosmicMappedRenderElement::Egui(elem) => elem.src(),
|
CosmicMappedRenderElement::Egui(elem) => elem.src(),
|
||||||
|
|
@ -733,8 +741,8 @@ where
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.geometry(scale),
|
CosmicMappedRenderElement::Stack(elem) => elem.geometry(scale),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.geometry(scale),
|
CosmicMappedRenderElement::Window(elem) => elem.geometry(scale),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.geometry(scale),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.geometry(scale),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.geometry(scale),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.geometry(scale),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => elem.geometry(scale),
|
CosmicMappedRenderElement::Indicator(elem) => elem.geometry(scale),
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
CosmicMappedRenderElement::Egui(elem) => elem.geometry(scale),
|
CosmicMappedRenderElement::Egui(elem) => elem.geometry(scale),
|
||||||
|
|
@ -745,8 +753,8 @@ where
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.location(scale),
|
CosmicMappedRenderElement::Stack(elem) => elem.location(scale),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.location(scale),
|
CosmicMappedRenderElement::Window(elem) => elem.location(scale),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.location(scale),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.location(scale),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.location(scale),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.location(scale),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => elem.location(scale),
|
CosmicMappedRenderElement::Indicator(elem) => elem.location(scale),
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
CosmicMappedRenderElement::Egui(elem) => elem.location(scale),
|
CosmicMappedRenderElement::Egui(elem) => elem.location(scale),
|
||||||
|
|
@ -757,8 +765,8 @@ where
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.transform(),
|
CosmicMappedRenderElement::Stack(elem) => elem.transform(),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.transform(),
|
CosmicMappedRenderElement::Window(elem) => elem.transform(),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.transform(),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.transform(),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.transform(),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.transform(),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => elem.transform(),
|
CosmicMappedRenderElement::Indicator(elem) => elem.transform(),
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
CosmicMappedRenderElement::Egui(elem) => elem.transform(),
|
CosmicMappedRenderElement::Egui(elem) => elem.transform(),
|
||||||
|
|
@ -773,8 +781,8 @@ where
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.damage_since(scale, commit),
|
CosmicMappedRenderElement::Stack(elem) => elem.damage_since(scale, commit),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.damage_since(scale, commit),
|
CosmicMappedRenderElement::Window(elem) => elem.damage_since(scale, commit),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.damage_since(scale, commit),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.damage_since(scale, commit),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.damage_since(scale, commit),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.damage_since(scale, commit),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => elem.damage_since(scale, commit),
|
CosmicMappedRenderElement::Indicator(elem) => elem.damage_since(scale, commit),
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
CosmicMappedRenderElement::Egui(elem) => elem.damage_since(scale, commit),
|
CosmicMappedRenderElement::Egui(elem) => elem.damage_since(scale, commit),
|
||||||
|
|
@ -785,8 +793,8 @@ where
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.opaque_regions(scale),
|
CosmicMappedRenderElement::Stack(elem) => elem.opaque_regions(scale),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.opaque_regions(scale),
|
CosmicMappedRenderElement::Window(elem) => elem.opaque_regions(scale),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.opaque_regions(scale),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.opaque_regions(scale),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.opaque_regions(scale),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.opaque_regions(scale),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => elem.opaque_regions(scale),
|
CosmicMappedRenderElement::Indicator(elem) => elem.opaque_regions(scale),
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
CosmicMappedRenderElement::Egui(elem) => elem.opaque_regions(scale),
|
CosmicMappedRenderElement::Egui(elem) => elem.opaque_regions(scale),
|
||||||
|
|
@ -797,8 +805,8 @@ where
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.alpha(),
|
CosmicMappedRenderElement::Stack(elem) => elem.alpha(),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.alpha(),
|
CosmicMappedRenderElement::Window(elem) => elem.alpha(),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.alpha(),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.alpha(),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.alpha(),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.alpha(),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => elem.alpha(),
|
CosmicMappedRenderElement::Indicator(elem) => elem.alpha(),
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
CosmicMappedRenderElement::Egui(elem) => elem.alpha(),
|
CosmicMappedRenderElement::Egui(elem) => elem.alpha(),
|
||||||
|
|
@ -817,8 +825,8 @@ impl RenderElement<GlowRenderer> for CosmicMappedRenderElement<GlowRenderer> {
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.draw(frame, src, dst, damage),
|
CosmicMappedRenderElement::Stack(elem) => elem.draw(frame, src, dst, damage),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.draw(frame, src, dst, damage),
|
CosmicMappedRenderElement::Window(elem) => elem.draw(frame, src, dst, damage),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.draw(frame, src, dst, damage),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.draw(frame, src, dst, damage),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.draw(frame, src, dst, damage),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.draw(frame, src, dst, damage),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => {
|
CosmicMappedRenderElement::Indicator(elem) => {
|
||||||
RenderElement::<GlowRenderer>::draw(elem, frame, src, dst, damage)
|
RenderElement::<GlowRenderer>::draw(elem, frame, src, dst, damage)
|
||||||
}
|
}
|
||||||
|
|
@ -833,8 +841,8 @@ impl RenderElement<GlowRenderer> for CosmicMappedRenderElement<GlowRenderer> {
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.underlying_storage(renderer),
|
CosmicMappedRenderElement::Stack(elem) => elem.underlying_storage(renderer),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.underlying_storage(renderer),
|
CosmicMappedRenderElement::Window(elem) => elem.underlying_storage(renderer),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.underlying_storage(renderer),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.underlying_storage(renderer),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.underlying_storage(renderer),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.underlying_storage(renderer),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => elem.underlying_storage(renderer),
|
CosmicMappedRenderElement::Indicator(elem) => elem.underlying_storage(renderer),
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
CosmicMappedRenderElement::Egui(elem) => elem.underlying_storage(renderer),
|
CosmicMappedRenderElement::Egui(elem) => elem.underlying_storage(renderer),
|
||||||
|
|
@ -855,8 +863,8 @@ impl<'a, 'b> RenderElement<GlMultiRenderer<'a, 'b>>
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.draw(frame, src, dst, damage),
|
CosmicMappedRenderElement::Stack(elem) => elem.draw(frame, src, dst, damage),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.draw(frame, src, dst, damage),
|
CosmicMappedRenderElement::Window(elem) => elem.draw(frame, src, dst, damage),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.draw(frame, src, dst, damage),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.draw(frame, src, dst, damage),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.draw(frame, src, dst, damage),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.draw(frame, src, dst, damage),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => {
|
CosmicMappedRenderElement::Indicator(elem) => {
|
||||||
RenderElement::<GlowRenderer>::draw(elem, frame.glow_frame_mut(), src, dst, damage)
|
RenderElement::<GlowRenderer>::draw(elem, frame.glow_frame_mut(), src, dst, damage)
|
||||||
.map_err(|err| MultiError::Render(err))
|
.map_err(|err| MultiError::Render(err))
|
||||||
|
|
@ -877,8 +885,8 @@ impl<'a, 'b> RenderElement<GlMultiRenderer<'a, 'b>>
|
||||||
match self {
|
match self {
|
||||||
CosmicMappedRenderElement::Stack(elem) => elem.underlying_storage(renderer),
|
CosmicMappedRenderElement::Stack(elem) => elem.underlying_storage(renderer),
|
||||||
CosmicMappedRenderElement::Window(elem) => elem.underlying_storage(renderer),
|
CosmicMappedRenderElement::Window(elem) => elem.underlying_storage(renderer),
|
||||||
CosmicMappedRenderElement::CroppedStack(elem) => elem.underlying_storage(renderer),
|
CosmicMappedRenderElement::TiledStack(elem) => elem.underlying_storage(renderer),
|
||||||
CosmicMappedRenderElement::CroppedWindow(elem) => elem.underlying_storage(renderer),
|
CosmicMappedRenderElement::TiledWindow(elem) => elem.underlying_storage(renderer),
|
||||||
CosmicMappedRenderElement::Indicator(elem) => {
|
CosmicMappedRenderElement::Indicator(elem) => {
|
||||||
elem.underlying_storage(renderer.glow_renderer_mut())
|
elem.underlying_storage(renderer.glow_renderer_mut())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,9 @@ impl From<KeyboardFocusTarget> for PointerFocusTarget {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct WindowGroup {
|
pub struct WindowGroup {
|
||||||
pub(in crate::shell) node: NodeId,
|
pub node: NodeId,
|
||||||
pub(in crate::shell) output: WeakOutput,
|
pub output: WeakOutput,
|
||||||
pub(in crate::shell) alive: Weak<()>,
|
pub alive: Weak<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for WindowGroup {
|
impl PartialEq for WindowGroup {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::render::{element::AsGlowRenderer, IndicatorShader},
|
backend::render::{element::AsGlowRenderer, IndicatorShader, FOCUS_INDICATOR_COLOR},
|
||||||
shell::{
|
shell::{
|
||||||
element::{window::CosmicWindowRenderElement, CosmicMapped, CosmicMappedRenderElement},
|
element::{window::CosmicWindowRenderElement, CosmicMapped, CosmicMappedRenderElement},
|
||||||
focus::target::{KeyboardFocusTarget, PointerFocusTarget},
|
focus::target::{KeyboardFocusTarget, PointerFocusTarget},
|
||||||
|
|
@ -66,9 +66,11 @@ impl MoveGrabState {
|
||||||
elements.push(
|
elements.push(
|
||||||
CosmicMappedRenderElement::from(IndicatorShader::element(
|
CosmicMappedRenderElement::from(IndicatorShader::element(
|
||||||
renderer,
|
renderer,
|
||||||
|
self.window.clone(),
|
||||||
Rectangle::from_loc_and_size(render_location, self.window.geometry().size),
|
Rectangle::from_loc_and_size(render_location, self.window.geometry().size),
|
||||||
self.indicator_thickness,
|
self.indicator_thickness,
|
||||||
1.0,
|
1.0,
|
||||||
|
FOCUS_INDICATOR_COLOR,
|
||||||
))
|
))
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use smithay::{
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::render::{element::AsGlowRenderer, IndicatorShader},
|
backend::render::{element::AsGlowRenderer, IndicatorShader, FOCUS_INDICATOR_COLOR},
|
||||||
shell::{
|
shell::{
|
||||||
element::{window::CosmicWindowRenderElement, CosmicMapped, CosmicMappedRenderElement},
|
element::{window::CosmicWindowRenderElement, CosmicMapped, CosmicMappedRenderElement},
|
||||||
grabs::ResizeEdge,
|
grabs::ResizeEdge,
|
||||||
|
|
@ -380,12 +380,14 @@ impl FloatingLayout {
|
||||||
if indicator_thickness > 0 {
|
if indicator_thickness > 0 {
|
||||||
let element = IndicatorShader::element(
|
let element = IndicatorShader::element(
|
||||||
renderer,
|
renderer,
|
||||||
|
elem.clone(),
|
||||||
Rectangle::from_loc_and_size(
|
Rectangle::from_loc_and_size(
|
||||||
self.space.element_location(elem).unwrap() - output_loc,
|
self.space.element_location(elem).unwrap() - output_loc,
|
||||||
elem.geometry().size,
|
elem.geometry().size,
|
||||||
),
|
),
|
||||||
indicator_thickness,
|
indicator_thickness,
|
||||||
1.0,
|
1.0,
|
||||||
|
FOCUS_INDICATOR_COLOR,
|
||||||
);
|
);
|
||||||
elements.insert(0, element.into());
|
elements.insert(0, element.into());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::render::{element::AsGlowRenderer, IndicatorShader},
|
backend::render::{
|
||||||
|
element::AsGlowRenderer, IndicatorShader, Key, ACTIVE_GROUP_COLOR, FOCUS_INDICATOR_COLOR,
|
||||||
|
GROUP_COLOR,
|
||||||
|
},
|
||||||
shell::{
|
shell::{
|
||||||
element::{window::CosmicWindowRenderElement, CosmicMapped, CosmicMappedRenderElement},
|
element::{window::CosmicWindowRenderElement, CosmicMapped, CosmicMappedRenderElement},
|
||||||
focus::{
|
focus::{
|
||||||
|
|
@ -23,7 +26,10 @@ use cosmic_time::{Cubic, Ease, Tween};
|
||||||
use id_tree::{InsertBehavior, MoveBehavior, Node, NodeId, NodeIdError, RemoveBehavior, Tree};
|
use id_tree::{InsertBehavior, MoveBehavior, Node, NodeId, NodeIdError, RemoveBehavior, Tree};
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::renderer::{
|
backend::renderer::{
|
||||||
element::{utils::CropRenderElement, AsRenderElements, RenderElement},
|
element::{
|
||||||
|
utils::{CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement},
|
||||||
|
AsRenderElements, RenderElement,
|
||||||
|
},
|
||||||
ImportAll, ImportMem, Renderer,
|
ImportAll, ImportMem, Renderer,
|
||||||
},
|
},
|
||||||
desktop::{layer_map_for_output, space::SpaceElement, PopupKind},
|
desktop::{layer_map_for_output, space::SpaceElement, PopupKind},
|
||||||
|
|
@ -1432,6 +1438,8 @@ impl TilingLayout {
|
||||||
renderer: &mut R,
|
renderer: &mut R,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
focused: Option<&CosmicMapped>,
|
focused: Option<&CosmicMapped>,
|
||||||
|
non_exclusive_zone: Rectangle<i32, Logical>,
|
||||||
|
draw_groups: bool,
|
||||||
indicator_thickness: u8,
|
indicator_thickness: u8,
|
||||||
) -> Result<Vec<CosmicMappedRenderElement<R>>, OutputNotMapped>
|
) -> Result<Vec<CosmicMappedRenderElement<R>>, OutputNotMapped>
|
||||||
where
|
where
|
||||||
|
|
@ -1473,213 +1481,589 @@ impl TilingLayout {
|
||||||
|
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
|
|
||||||
// all old windows and fade them out
|
// all gone windows and fade them out
|
||||||
if let Some(reference_tree) = reference_tree.as_ref() {
|
let old_geometries = if let Some(reference_tree) = reference_tree.as_ref() {
|
||||||
if let Some(root) = reference_tree.root_node_id() {
|
let (geometries, _) = if draw_groups {
|
||||||
elements.extend(
|
geometries_for_groupview(
|
||||||
reference_tree
|
reference_tree,
|
||||||
.traverse_pre_order(root)
|
renderer,
|
||||||
.unwrap()
|
non_exclusive_zone,
|
||||||
.filter(|node| node.data().is_mapped(None))
|
focused, // TODO: Would be better to be an old focus,
|
||||||
.map(|node| match node.data() {
|
// but for that we have to associate focus with a tree (and animate focus changes properly)
|
||||||
Data::Mapped {
|
1.0 - percentage,
|
||||||
mapped,
|
|
||||||
last_geometry,
|
|
||||||
..
|
|
||||||
} => (mapped, last_geometry),
|
|
||||||
_ => unreachable!(),
|
|
||||||
})
|
|
||||||
.filter(|(mapped, _)| {
|
|
||||||
if let Some(root) = target_tree.root_node_id() {
|
|
||||||
!target_tree
|
|
||||||
.traverse_pre_order(root)
|
|
||||||
.unwrap()
|
|
||||||
.any(|node| node.data().is_mapped(Some(mapped)))
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flat_map(|(mapped, geo)| {
|
|
||||||
let crop_rect = geo.clone();
|
|
||||||
AsRenderElements::<R>::render_elements::<CosmicMappedRenderElement<R>>(
|
|
||||||
mapped,
|
|
||||||
renderer,
|
|
||||||
geo.loc.to_physical_precise_round(output_scale)
|
|
||||||
- mapped
|
|
||||||
.geometry()
|
|
||||||
.loc
|
|
||||||
.to_physical_precise_round(output_scale),
|
|
||||||
Scale::from(output_scale),
|
|
||||||
1.0 - percentage,
|
|
||||||
)
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|element| match element {
|
|
||||||
CosmicMappedRenderElement::Stack(elem) => {
|
|
||||||
CropRenderElement::from_element(
|
|
||||||
elem,
|
|
||||||
output_scale,
|
|
||||||
crop_rect.to_physical_precise_round(output_scale),
|
|
||||||
)
|
|
||||||
.map(CosmicMappedRenderElement::CroppedStack)
|
|
||||||
}
|
|
||||||
CosmicMappedRenderElement::Window(elem) => {
|
|
||||||
CropRenderElement::from_element(
|
|
||||||
elem,
|
|
||||||
output_scale,
|
|
||||||
crop_rect.to_physical_precise_round(output_scale),
|
|
||||||
)
|
|
||||||
.map(CosmicMappedRenderElement::CroppedWindow)
|
|
||||||
}
|
|
||||||
x => Some(x),
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
.unzip();
|
||||||
|
|
||||||
if let Some(root) = target_tree.root_node_id() {
|
// all old windows we want to fade out
|
||||||
elements.extend(
|
elements.extend(render_old_tree(
|
||||||
target_tree
|
reference_tree,
|
||||||
.traverse_pre_order(root)
|
target_tree,
|
||||||
.unwrap()
|
renderer,
|
||||||
.filter(|node| node.data().is_mapped(None))
|
geometries.clone(),
|
||||||
.filter(|node| match node.data() {
|
output_scale,
|
||||||
Data::Mapped { mapped, .. } => mapped.is_activated(),
|
percentage,
|
||||||
_ => unreachable!(),
|
));
|
||||||
})
|
|
||||||
.map(|node| match node.data() {
|
|
||||||
Data::Mapped {
|
|
||||||
mapped,
|
|
||||||
last_geometry,
|
|
||||||
..
|
|
||||||
} => (mapped, last_geometry),
|
|
||||||
_ => unreachable!(),
|
|
||||||
})
|
|
||||||
.chain(
|
|
||||||
target_tree
|
|
||||||
.traverse_pre_order(root)
|
|
||||||
.unwrap()
|
|
||||||
.filter(|node| node.data().is_mapped(None))
|
|
||||||
.filter(|node| match node.data() {
|
|
||||||
Data::Mapped { mapped, .. } => !mapped.is_activated(),
|
|
||||||
_ => unreachable!(),
|
|
||||||
})
|
|
||||||
.map(|node| match node.data() {
|
|
||||||
Data::Mapped {
|
|
||||||
mapped,
|
|
||||||
last_geometry,
|
|
||||||
..
|
|
||||||
} => (mapped, last_geometry),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.flat_map(|(mapped, new_geo)| {
|
|
||||||
let old_geo = if let Some(reference_tree) = reference_tree.as_ref() {
|
|
||||||
if let Some(root) = reference_tree.root_node_id() {
|
|
||||||
reference_tree
|
|
||||||
.traverse_pre_order(root)
|
|
||||||
.unwrap()
|
|
||||||
.find(|node| node.data().is_mapped(Some(mapped)))
|
|
||||||
.map(|node| match node.data() {
|
|
||||||
Data::Mapped { last_geometry, .. } => last_geometry,
|
|
||||||
_ => unreachable!(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let (geo, alpha) = if let Some(old_geo) = old_geo {
|
geometries
|
||||||
(
|
} else {
|
||||||
Rectangle::from_loc_and_size(
|
None
|
||||||
(
|
};
|
||||||
old_geo.loc.x
|
|
||||||
+ ((new_geo.loc.x - old_geo.loc.x) as f32 * percentage)
|
|
||||||
.round()
|
|
||||||
as i32,
|
|
||||||
old_geo.loc.y
|
|
||||||
+ ((new_geo.loc.y - old_geo.loc.y) as f32 * percentage)
|
|
||||||
.round()
|
|
||||||
as i32,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
old_geo.size.w
|
|
||||||
+ ((new_geo.size.w - old_geo.size.w) as f32
|
|
||||||
* percentage)
|
|
||||||
.round()
|
|
||||||
as i32,
|
|
||||||
old_geo.size.h
|
|
||||||
+ ((new_geo.size.h - old_geo.size.h) as f32
|
|
||||||
* percentage)
|
|
||||||
.round()
|
|
||||||
as i32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
1.0,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// TODO: If old_geo.is_none() animate alpha - fade in
|
|
||||||
(*new_geo, percentage)
|
|
||||||
};
|
|
||||||
|
|
||||||
if alpha < 1.0 {
|
let (geometries, group_elements) = if draw_groups {
|
||||||
dbg!(alpha);
|
geometries_for_groupview(
|
||||||
}
|
target_tree,
|
||||||
|
renderer,
|
||||||
let crop_rect = geo.clone();
|
non_exclusive_zone,
|
||||||
let mut elements =
|
focused,
|
||||||
AsRenderElements::<R>::render_elements::<CosmicMappedRenderElement<R>>(
|
percentage,
|
||||||
mapped,
|
|
||||||
renderer,
|
|
||||||
geo.loc.to_physical_precise_round(output_scale)
|
|
||||||
- mapped
|
|
||||||
.geometry()
|
|
||||||
.loc
|
|
||||||
.to_physical_precise_round(output_scale),
|
|
||||||
Scale::from(output_scale),
|
|
||||||
alpha,
|
|
||||||
)
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|element| match element {
|
|
||||||
CosmicMappedRenderElement::Stack(elem) => {
|
|
||||||
CropRenderElement::from_element(
|
|
||||||
elem,
|
|
||||||
output_scale,
|
|
||||||
crop_rect.to_physical_precise_round(output_scale),
|
|
||||||
)
|
|
||||||
.map(CosmicMappedRenderElement::CroppedStack)
|
|
||||||
}
|
|
||||||
CosmicMappedRenderElement::Window(elem) => {
|
|
||||||
CropRenderElement::from_element(
|
|
||||||
elem,
|
|
||||||
output_scale,
|
|
||||||
crop_rect.to_physical_precise_round(output_scale),
|
|
||||||
)
|
|
||||||
.map(CosmicMappedRenderElement::CroppedWindow)
|
|
||||||
}
|
|
||||||
x => Some(x),
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if focused == Some(mapped) {
|
|
||||||
if indicator_thickness > 0 {
|
|
||||||
let element = IndicatorShader::element(
|
|
||||||
renderer,
|
|
||||||
geo,
|
|
||||||
indicator_thickness,
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
elements.insert(0, element.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elements
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
// tiling hints
|
||||||
|
if let Some(group_elements) = group_elements {
|
||||||
|
elements.extend(group_elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
// all alive windows
|
||||||
|
elements.extend(render_new_tree(
|
||||||
|
target_tree,
|
||||||
|
reference_tree,
|
||||||
|
renderer,
|
||||||
|
geometries,
|
||||||
|
old_geometries,
|
||||||
|
focused,
|
||||||
|
output_scale,
|
||||||
|
percentage,
|
||||||
|
if draw_groups { 3 } else { indicator_thickness },
|
||||||
|
));
|
||||||
|
|
||||||
Ok(elements)
|
Ok(elements)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn geometries_for_groupview<R>(
|
||||||
|
tree: &Tree<Data>,
|
||||||
|
renderer: &mut R,
|
||||||
|
non_exclusive_zone: Rectangle<i32, Logical>,
|
||||||
|
focused: Option<&CosmicMapped>,
|
||||||
|
alpha: f32,
|
||||||
|
) -> Option<(
|
||||||
|
HashMap<NodeId, Rectangle<i32, Logical>>,
|
||||||
|
Vec<CosmicMappedRenderElement<R>>,
|
||||||
|
)>
|
||||||
|
where
|
||||||
|
R: Renderer + ImportAll + ImportMem + AsGlowRenderer,
|
||||||
|
<R as Renderer>::TextureId: 'static,
|
||||||
|
CosmicMappedRenderElement<R>: RenderElement<R>,
|
||||||
|
CosmicWindowRenderElement<R>: RenderElement<R>,
|
||||||
|
{
|
||||||
|
// we need to recalculate geometry for all elements, if we are drawing groups
|
||||||
|
if let Some(root) = tree.root_node_id() {
|
||||||
|
let mut stack = vec![non_exclusive_zone];
|
||||||
|
let mut elements = Vec::new();
|
||||||
|
let mut geometries = HashMap::new();
|
||||||
|
|
||||||
|
const GAP: i32 = 16;
|
||||||
|
for node_id in tree.traverse_pre_order_ids(root).unwrap() {
|
||||||
|
if let Some(mut geo) = stack.pop() {
|
||||||
|
// zoom in windows
|
||||||
|
geo.loc += (GAP, GAP).into();
|
||||||
|
geo.size -= (GAP * 2, GAP * 2).into();
|
||||||
|
|
||||||
|
let node: &Node<Data> = tree.get(&node_id).unwrap();
|
||||||
|
let data = node.data();
|
||||||
|
|
||||||
|
let is_potential_group = if let Some(focused) = focused {
|
||||||
|
// 1. focused can move into us directly
|
||||||
|
if let Some(parent) = node.parent() {
|
||||||
|
let parent_data = tree.get(parent).unwrap().data();
|
||||||
|
|
||||||
|
let idx = tree
|
||||||
|
.children_ids(parent)
|
||||||
|
.unwrap()
|
||||||
|
.position(|id| id == &node_id)
|
||||||
|
.unwrap();
|
||||||
|
if let Some((focused_idx, _focused_id)) = tree
|
||||||
|
.children_ids(parent)
|
||||||
|
.unwrap()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, child_id)| {
|
||||||
|
tree.get(child_id).unwrap().data().is_mapped(Some(focused))
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// only direct neighbors
|
||||||
|
focused_idx.abs_diff(idx) == 1
|
||||||
|
// skip neighbors, if this is a group of two windows
|
||||||
|
&& !(parent_data.len() == 2 && data.is_mapped(None))
|
||||||
|
// skip groups of two in opposite orientation to indicate move between
|
||||||
|
&& !(parent_data.len() == 2 && if data.is_group() { parent_data.orientation() != data.orientation() } else { false } )
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. focused can move out into us
|
||||||
|
else {
|
||||||
|
tree.children_ids(&node_id)
|
||||||
|
.unwrap()
|
||||||
|
.find(|child_id| {
|
||||||
|
tree.children(child_id)
|
||||||
|
.unwrap()
|
||||||
|
.any(|child| child.data().is_mapped(Some(focused)))
|
||||||
|
})
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
match data {
|
||||||
|
Data::Group {
|
||||||
|
orientation,
|
||||||
|
last_geometry,
|
||||||
|
sizes,
|
||||||
|
alive,
|
||||||
|
} => {
|
||||||
|
let has_active_child = if let Some(focused) = focused {
|
||||||
|
tree.children(&node_id)
|
||||||
|
.unwrap()
|
||||||
|
.any(|child| child.data().is_mapped(Some(focused)))
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (is_potential_group || has_active_child) && &node_id != root {
|
||||||
|
elements.push(
|
||||||
|
IndicatorShader::element(
|
||||||
|
renderer,
|
||||||
|
Key::Group(Arc::downgrade(&alive)),
|
||||||
|
geo,
|
||||||
|
3,
|
||||||
|
alpha,
|
||||||
|
if has_active_child {
|
||||||
|
ACTIVE_GROUP_COLOR
|
||||||
|
} else {
|
||||||
|
GROUP_COLOR
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
geometries.insert(node_id.clone(), geo);
|
||||||
|
|
||||||
|
let previous_length = match orientation {
|
||||||
|
Orientation::Horizontal => last_geometry.size.h,
|
||||||
|
Orientation::Vertical => last_geometry.size.w,
|
||||||
|
};
|
||||||
|
let new_length = match orientation {
|
||||||
|
Orientation::Horizontal => geo.size.h,
|
||||||
|
Orientation::Vertical => geo.size.w,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut sizes = sizes
|
||||||
|
.iter()
|
||||||
|
.map(|len| {
|
||||||
|
(((*len as f64) / (previous_length as f64)) * (new_length as f64))
|
||||||
|
.round() as i32
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let sum: i32 = sizes.iter().sum();
|
||||||
|
if sum < new_length {
|
||||||
|
*sizes.last_mut().unwrap() += new_length - sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
match orientation {
|
||||||
|
Orientation::Horizontal => {
|
||||||
|
let mut previous: i32 = sizes.iter().sum();
|
||||||
|
for size in sizes.iter().rev() {
|
||||||
|
previous -= *size;
|
||||||
|
stack.push(Rectangle::from_loc_and_size(
|
||||||
|
(geo.loc.x, geo.loc.y + previous),
|
||||||
|
(geo.size.w, *size),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Orientation::Vertical => {
|
||||||
|
let mut previous: i32 = sizes.iter().sum();
|
||||||
|
for size in sizes.iter().rev() {
|
||||||
|
previous -= *size;
|
||||||
|
stack.push(Rectangle::from_loc_and_size(
|
||||||
|
(geo.loc.x + previous, geo.loc.y),
|
||||||
|
(*size, geo.size.h),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Data::Mapped { mapped, .. } => {
|
||||||
|
if is_potential_group {
|
||||||
|
elements.push(
|
||||||
|
IndicatorShader::element(
|
||||||
|
renderer,
|
||||||
|
mapped.clone(),
|
||||||
|
geo,
|
||||||
|
3,
|
||||||
|
alpha,
|
||||||
|
GROUP_COLOR,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
geo.loc += (GAP, GAP).into();
|
||||||
|
geo.size -= (GAP * 2, GAP * 2).into();
|
||||||
|
}
|
||||||
|
|
||||||
|
geometries.insert(node_id.clone(), geo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((geometries, elements))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_old_tree<R>(
|
||||||
|
reference_tree: &Tree<Data>,
|
||||||
|
target_tree: &Tree<Data>,
|
||||||
|
renderer: &mut R,
|
||||||
|
geometries: Option<HashMap<NodeId, Rectangle<i32, Logical>>>,
|
||||||
|
output_scale: f64,
|
||||||
|
percentage: f32,
|
||||||
|
) -> Vec<CosmicMappedRenderElement<R>>
|
||||||
|
where
|
||||||
|
R: Renderer + ImportAll + ImportMem + AsGlowRenderer,
|
||||||
|
<R as Renderer>::TextureId: 'static,
|
||||||
|
CosmicMappedRenderElement<R>: RenderElement<R>,
|
||||||
|
CosmicWindowRenderElement<R>: RenderElement<R>,
|
||||||
|
{
|
||||||
|
if let Some(root) = reference_tree.root_node_id() {
|
||||||
|
let geometries = geometries.unwrap_or_default();
|
||||||
|
reference_tree
|
||||||
|
.traverse_pre_order_ids(root)
|
||||||
|
.unwrap()
|
||||||
|
.filter(|node_id| reference_tree.get(node_id).unwrap().data().is_mapped(None))
|
||||||
|
.map(
|
||||||
|
|node_id| match reference_tree.get(&node_id).unwrap().data() {
|
||||||
|
Data::Mapped {
|
||||||
|
mapped,
|
||||||
|
last_geometry,
|
||||||
|
..
|
||||||
|
} => (mapped, last_geometry, geometries.get(&node_id)),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.filter(|(mapped, _, _)| {
|
||||||
|
if let Some(root) = target_tree.root_node_id() {
|
||||||
|
!target_tree
|
||||||
|
.traverse_pre_order(root)
|
||||||
|
.unwrap()
|
||||||
|
.any(|node| node.data().is_mapped(Some(mapped)))
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flat_map(|(mapped, original_geo, scaled_geo)| {
|
||||||
|
let (scale, offset) = scaled_geo
|
||||||
|
.map(|adapted_geo| scale_to_center(&original_geo, adapted_geo))
|
||||||
|
.unwrap_or_else(|| (1.0.into(), (0, 0).into()));
|
||||||
|
let geo = scaled_geo
|
||||||
|
.map(|adapted_geo| {
|
||||||
|
Rectangle::from_loc_and_size(
|
||||||
|
adapted_geo.loc + offset,
|
||||||
|
(
|
||||||
|
(original_geo.size.w as f64 * scale).round() as i32,
|
||||||
|
(original_geo.size.h as f64 * scale).round() as i32,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or(*original_geo);
|
||||||
|
|
||||||
|
let crop_rect = geo.clone();
|
||||||
|
let original_location = original_geo.loc.to_physical_precise_round(output_scale)
|
||||||
|
- mapped
|
||||||
|
.geometry()
|
||||||
|
.loc
|
||||||
|
.to_physical_precise_round(output_scale);
|
||||||
|
AsRenderElements::<R>::render_elements::<CosmicMappedRenderElement<R>>(
|
||||||
|
mapped,
|
||||||
|
renderer,
|
||||||
|
original_location,
|
||||||
|
Scale::from(output_scale),
|
||||||
|
1.0 - percentage,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|element| match element {
|
||||||
|
CosmicMappedRenderElement::Stack(elem) => Some(
|
||||||
|
CosmicMappedRenderElement::TiledStack(RelocateRenderElement::from_element(
|
||||||
|
RescaleRenderElement::from_element(
|
||||||
|
CropRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
output_scale,
|
||||||
|
crop_rect.to_physical_precise_round(output_scale),
|
||||||
|
)?,
|
||||||
|
original_location,
|
||||||
|
scale,
|
||||||
|
),
|
||||||
|
geo.loc.to_physical_precise_round(output_scale),
|
||||||
|
Relocate::Absolute,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
CosmicMappedRenderElement::Window(elem) => {
|
||||||
|
Some(CosmicMappedRenderElement::TiledWindow(
|
||||||
|
RelocateRenderElement::from_element(
|
||||||
|
RescaleRenderElement::from_element(
|
||||||
|
CropRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
output_scale,
|
||||||
|
crop_rect.to_physical_precise_round(output_scale),
|
||||||
|
)?,
|
||||||
|
(0, 0).into(),
|
||||||
|
scale,
|
||||||
|
),
|
||||||
|
geo.loc.to_physical_precise_round(output_scale),
|
||||||
|
Relocate::Absolute,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
x => Some(x),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_new_tree<R>(
|
||||||
|
target_tree: &Tree<Data>,
|
||||||
|
reference_tree: Option<&Tree<Data>>,
|
||||||
|
renderer: &mut R,
|
||||||
|
geometries: Option<HashMap<NodeId, Rectangle<i32, Logical>>>,
|
||||||
|
old_geometries: Option<HashMap<NodeId, Rectangle<i32, Logical>>>,
|
||||||
|
focused: Option<&CosmicMapped>,
|
||||||
|
output_scale: f64,
|
||||||
|
percentage: f32,
|
||||||
|
indicator_thickness: u8,
|
||||||
|
) -> Vec<CosmicMappedRenderElement<R>>
|
||||||
|
where
|
||||||
|
R: Renderer + ImportAll + ImportMem + AsGlowRenderer,
|
||||||
|
<R as Renderer>::TextureId: 'static,
|
||||||
|
CosmicMappedRenderElement<R>: RenderElement<R>,
|
||||||
|
CosmicWindowRenderElement<R>: RenderElement<R>,
|
||||||
|
{
|
||||||
|
if let Some(root) = target_tree.root_node_id() {
|
||||||
|
let old_geometries = old_geometries.unwrap_or_default();
|
||||||
|
let geometries = geometries.unwrap_or_default();
|
||||||
|
target_tree
|
||||||
|
.traverse_pre_order_ids(root)
|
||||||
|
.unwrap()
|
||||||
|
.filter(|node_id| target_tree.get(node_id).unwrap().data().is_mapped(None))
|
||||||
|
.map(|node_id| match target_tree.get(&node_id).unwrap().data() {
|
||||||
|
Data::Mapped {
|
||||||
|
mapped,
|
||||||
|
last_geometry,
|
||||||
|
..
|
||||||
|
} => (mapped, last_geometry, geometries.get(&node_id)),
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.flat_map(|(mapped, original_geo, scaled_geo)| {
|
||||||
|
let (old_original_geo, old_scaled_geo) =
|
||||||
|
if let Some(reference_tree) = reference_tree.as_ref() {
|
||||||
|
if let Some(root) = reference_tree.root_node_id() {
|
||||||
|
reference_tree
|
||||||
|
.traverse_pre_order_ids(root)
|
||||||
|
.unwrap()
|
||||||
|
.find(|node_id| {
|
||||||
|
reference_tree
|
||||||
|
.get(node_id)
|
||||||
|
.unwrap()
|
||||||
|
.data()
|
||||||
|
.is_mapped(Some(mapped))
|
||||||
|
})
|
||||||
|
.map(
|
||||||
|
|node_id| match reference_tree.get(&node_id).unwrap().data() {
|
||||||
|
Data::Mapped { last_geometry, .. } => {
|
||||||
|
(last_geometry, old_geometries.get(&node_id))
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
.unzip();
|
||||||
|
let old_geo = old_original_geo.map(|original_geo| {
|
||||||
|
let (scale, offset) = old_scaled_geo
|
||||||
|
.unwrap()
|
||||||
|
.map(|adapted_geo| scale_to_center(original_geo, adapted_geo))
|
||||||
|
.unwrap_or_else(|| (1.0.into(), (0, 0).into()));
|
||||||
|
old_scaled_geo
|
||||||
|
.unwrap()
|
||||||
|
.map(|adapted_geo| {
|
||||||
|
Rectangle::from_loc_and_size(
|
||||||
|
adapted_geo.loc + offset,
|
||||||
|
(
|
||||||
|
(original_geo.size.w as f64 * scale).round() as i32,
|
||||||
|
(original_geo.size.h as f64 * scale).round() as i32,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or(*original_geo)
|
||||||
|
});
|
||||||
|
|
||||||
|
let crop_rect = original_geo;
|
||||||
|
let (scale, offset) = scaled_geo
|
||||||
|
.map(|adapted_geo| scale_to_center(original_geo, adapted_geo))
|
||||||
|
.unwrap_or_else(|| (1.0.into(), (0, 0).into()));
|
||||||
|
let new_geo = scaled_geo
|
||||||
|
.map(|adapted_geo| {
|
||||||
|
Rectangle::from_loc_and_size(
|
||||||
|
adapted_geo.loc + offset,
|
||||||
|
(
|
||||||
|
(original_geo.size.w as f64 * scale).round() as i32,
|
||||||
|
(original_geo.size.h as f64 * scale).round() as i32,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or(*original_geo);
|
||||||
|
|
||||||
|
let (geo, alpha) = if let Some(old_geo) = old_geo {
|
||||||
|
(
|
||||||
|
Rectangle::from_loc_and_size(
|
||||||
|
(
|
||||||
|
old_geo.loc.x
|
||||||
|
+ ((new_geo.loc.x - old_geo.loc.x) as f32 * percentage).round()
|
||||||
|
as i32,
|
||||||
|
old_geo.loc.y
|
||||||
|
+ ((new_geo.loc.y - old_geo.loc.y) as f32 * percentage).round()
|
||||||
|
as i32,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
old_geo.size.w
|
||||||
|
+ ((new_geo.size.w - old_geo.size.w) as f32 * percentage)
|
||||||
|
.round() as i32,
|
||||||
|
old_geo.size.h
|
||||||
|
+ ((new_geo.size.h - old_geo.size.h) as f32 * percentage)
|
||||||
|
.round() as i32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(new_geo, percentage)
|
||||||
|
};
|
||||||
|
|
||||||
|
let original_location = original_geo.loc.to_physical_precise_round(output_scale)
|
||||||
|
- mapped
|
||||||
|
.geometry()
|
||||||
|
.loc
|
||||||
|
.to_physical_precise_round(output_scale);
|
||||||
|
let mut elements = AsRenderElements::<R>::render_elements::<
|
||||||
|
CosmicMappedRenderElement<R>,
|
||||||
|
>(
|
||||||
|
mapped,
|
||||||
|
renderer,
|
||||||
|
original_location,
|
||||||
|
Scale::from(output_scale),
|
||||||
|
alpha,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|element| match element {
|
||||||
|
CosmicMappedRenderElement::Stack(elem) => Some(
|
||||||
|
CosmicMappedRenderElement::TiledStack(RelocateRenderElement::from_element(
|
||||||
|
RescaleRenderElement::from_element(
|
||||||
|
CropRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
output_scale,
|
||||||
|
crop_rect.to_physical_precise_round(output_scale),
|
||||||
|
)?,
|
||||||
|
original_location,
|
||||||
|
scale,
|
||||||
|
),
|
||||||
|
geo.loc.to_physical_precise_round(output_scale),
|
||||||
|
Relocate::Absolute,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
CosmicMappedRenderElement::Window(elem) => {
|
||||||
|
Some(CosmicMappedRenderElement::TiledWindow(
|
||||||
|
RelocateRenderElement::from_element(
|
||||||
|
RescaleRenderElement::from_element(
|
||||||
|
CropRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
output_scale,
|
||||||
|
crop_rect.to_physical_precise_round(output_scale),
|
||||||
|
)?,
|
||||||
|
(0, 0).into(),
|
||||||
|
scale,
|
||||||
|
),
|
||||||
|
geo.loc.to_physical_precise_round(output_scale),
|
||||||
|
Relocate::Absolute,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
x => Some(x),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if focused == Some(mapped) {
|
||||||
|
if indicator_thickness > 0 {
|
||||||
|
let element = IndicatorShader::element(
|
||||||
|
renderer,
|
||||||
|
mapped.clone(),
|
||||||
|
geo,
|
||||||
|
indicator_thickness,
|
||||||
|
1.0,
|
||||||
|
FOCUS_INDICATOR_COLOR,
|
||||||
|
);
|
||||||
|
elements.insert(0, element.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elements
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scale_to_center(
|
||||||
|
old_geo: &Rectangle<i32, Logical>,
|
||||||
|
new_geo: &Rectangle<i32, Logical>,
|
||||||
|
) -> (f64, Point<i32, Logical>) {
|
||||||
|
let scale_w = new_geo.size.w as f64 / old_geo.size.w as f64;
|
||||||
|
let scale_h = new_geo.size.h as f64 / old_geo.size.h as f64;
|
||||||
|
|
||||||
|
if scale_w > scale_h {
|
||||||
|
(
|
||||||
|
scale_h,
|
||||||
|
(
|
||||||
|
((new_geo.size.w as f64 - old_geo.size.w as f64 * scale_h) / 2.0).round() as i32,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
scale_w,
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
((new_geo.size.h as f64 - old_geo.size.h as f64 * scale_w) / 2.0).round() as i32,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -454,6 +454,7 @@ impl Workspace {
|
||||||
override_redirect_windows: &[X11Surface],
|
override_redirect_windows: &[X11Surface],
|
||||||
xwm_state: Option<&'a mut XWaylandState>,
|
xwm_state: Option<&'a mut XWaylandState>,
|
||||||
draw_focus_indicator: Option<&Seat<State>>,
|
draw_focus_indicator: Option<&Seat<State>>,
|
||||||
|
draw_groups: bool,
|
||||||
indicator_thickness: u8,
|
indicator_thickness: u8,
|
||||||
exclude_workspace_overview: bool,
|
exclude_workspace_overview: bool,
|
||||||
) -> Result<Vec<WorkspaceRenderElement<R>>, OutputNotMapped>
|
) -> Result<Vec<WorkspaceRenderElement<R>>, OutputNotMapped>
|
||||||
|
|
@ -604,7 +605,14 @@ impl Workspace {
|
||||||
//tiling surfaces
|
//tiling surfaces
|
||||||
render_elements.extend(
|
render_elements.extend(
|
||||||
self.tiling_layer
|
self.tiling_layer
|
||||||
.render_output::<R>(renderer, output, focused.as_ref(), indicator_thickness)?
|
.render_output::<R>(
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
focused.as_ref(),
|
||||||
|
layer_map.non_exclusive_zone(),
|
||||||
|
draw_groups,
|
||||||
|
indicator_thickness,
|
||||||
|
)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(WorkspaceRenderElement::from),
|
.map(WorkspaceRenderElement::from),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue