aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-02-21 21:27:44 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-02-21 22:15:21 +0400
commit48f0f6fb3ceb68fdb559ab38c8dcbb7b9ba3a13e (patch)
treed71dbc10132f38f59e4a72d1d8a509102f701f8c
parent340bac0690972ee6f6badc8d386162320d70b7bd (diff)
downloadniri-48f0f6fb3ceb68fdb559ab38c8dcbb7b9ba3a13e.tar.gz
niri-48f0f6fb3ceb68fdb559ab38c8dcbb7b9ba3a13e.tar.bz2
niri-48f0f6fb3ceb68fdb559ab38c8dcbb7b9ba3a13e.zip
Implement gradient borders
-rw-r--r--Cargo.lock58
-rw-r--r--niri-config/Cargo.toml1
-rw-r--r--niri-config/src/lib.rs56
-rw-r--r--niri-visual-tests/src/cases/layout.rs2
-rw-r--r--niri-visual-tests/src/cases/tile.rs8
-rw-r--r--resources/default-config.kdl21
-rw-r--r--src/backend/tty.rs3
-rw-r--r--src/backend/winit.rs10
-rw-r--r--src/layout/focus_ring.rs101
-rw-r--r--src/layout/tile.rs22
-rw-r--r--src/layout/workspace.rs2
-rw-r--r--src/render_helpers/gradient.rs143
-rw-r--r--src/render_helpers/mod.rs3
-rw-r--r--src/render_helpers/primary_gpu_pixel_shader.rs97
-rw-r--r--src/render_helpers/shaders/gradient_border.frag46
-rw-r--r--src/render_helpers/shaders/mod.rs47
16 files changed, 584 insertions, 36 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2a3bb2c5..e6ab6b71 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -763,6 +763,15 @@ dependencies = [
]
[[package]]
+name = "csscolorparser"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf"
+dependencies = [
+ "phf",
+]
+
+[[package]]
name = "cursor-icon"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2128,6 +2137,7 @@ name = "niri-config"
version = "0.1.2"
dependencies = [
"bitflags 2.4.2",
+ "csscolorparser",
"knuffel",
"miette",
"niri-ipc",
@@ -2409,6 +2419,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
+name = "phf"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
name = "pin-project-lite"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2990,6 +3042,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
+[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/niri-config/Cargo.toml b/niri-config/Cargo.toml
index 944c8b1e..fed9993e 100644
--- a/niri-config/Cargo.toml
+++ b/niri-config/Cargo.toml
@@ -9,6 +9,7 @@ repository.workspace = true
[dependencies]
bitflags.workspace = true
+csscolorparser = "0.6.2"
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "0.1.2", path = "../niri-ipc" }
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index fdf3d700..77642813 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -340,6 +340,10 @@ pub struct FocusRing {
pub active_color: Color,
#[knuffel(child, default = Self::default().inactive_color)]
pub inactive_color: Color,
+ #[knuffel(child)]
+ pub active_gradient: Option<Gradient>,
+ #[knuffel(child)]
+ pub inactive_gradient: Option<Gradient>,
}
impl Default for FocusRing {
@@ -349,11 +353,32 @@ impl Default for FocusRing {
width: 4,
active_color: Color::new(127, 200, 255, 255),
inactive_color: Color::new(80, 80, 80, 255),
+ active_gradient: None,
+ inactive_gradient: None,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
+pub struct Gradient {
+ #[knuffel(property, str)]
+ pub from: Color,
+ #[knuffel(property, str)]
+ pub to: Color,
+ #[knuffel(property, default = 180)]
+ pub angle: i16,
+ #[knuffel(property, default)]
+ pub relative_to: GradientRelativeTo,
+}
+
+#[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub enum GradientRelativeTo {
+ #[default]
+ Window,
+ WorkspaceView,
+}
+
+#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct Border {
#[knuffel(child)]
pub off: bool,
@@ -363,6 +388,10 @@ pub struct Border {
pub active_color: Color,
#[knuffel(child, default = Self::default().inactive_color)]
pub inactive_color: Color,
+ #[knuffel(child)]
+ pub active_gradient: Option<Gradient>,
+ #[knuffel(child)]
+ pub inactive_gradient: Option<Gradient>,
}
impl Default for Border {
@@ -372,6 +401,8 @@ impl Default for Border {
width: 4,
active_color: Color::new(255, 200, 127, 255),
inactive_color: Color::new(80, 80, 80, 255),
+ active_gradient: None,
+ inactive_gradient: None,
}
}
}
@@ -383,6 +414,8 @@ impl From<Border> for FocusRing {
width: value.width,
active_color: value.active_color,
inactive_color: value.inactive_color,
+ active_gradient: value.active_gradient,
+ inactive_gradient: value.inactive_gradient,
}
}
}
@@ -790,6 +823,15 @@ impl Default for Config {
}
}
+impl FromStr for Color {
+ type Err = miette::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let [r, g, b, a] = csscolorparser::parse(s).into_diagnostic()?.to_rgba8();
+ Ok(Self { r, g, b, a })
+ }
+}
+
impl FromStr for Mode {
type Err = miette::Error;
@@ -909,7 +951,7 @@ mod tests {
#[test]
fn parse() {
check(
- r#"
+ r##"
input {
keyboard {
repeat-delay 600
@@ -961,6 +1003,7 @@ mod tests {
width 5
active-color 0 100 200 255
inactive-color 255 200 100 0
+ active-gradient from="rgba(10, 20, 30, 1.0)" to="#0080ffff" relative-to="workspace-view"
}
border {
@@ -1034,7 +1077,7 @@ mod tests {
debug {
render-drm-device "/dev/dri/renderD129"
}
- "#,
+ "##,
Config {
input: Input {
keyboard: Keyboard {
@@ -1099,6 +1142,13 @@ mod tests {
b: 100,
a: 0,
},
+ active_gradient: Some(Gradient {
+ from: Color::new(10, 20, 30, 255),
+ to: Color::new(0, 128, 255, 255),
+ angle: 180,
+ relative_to: GradientRelativeTo::WorkspaceView,
+ }),
+ inactive_gradient: None,
},
border: Border {
off: false,
@@ -1115,6 +1165,8 @@ mod tests {
b: 100,
a: 0,
},
+ active_gradient: None,
+ inactive_gradient: None,
},
preset_column_widths: vec![
PresetWidth::Proportion(0.25),
diff --git a/niri-visual-tests/src/cases/layout.rs b/niri-visual-tests/src/cases/layout.rs
index dd9e29ec..ca8edcec 100644
--- a/niri-visual-tests/src/cases/layout.rs
+++ b/niri-visual-tests/src/cases/layout.rs
@@ -51,6 +51,8 @@ impl Layout {
width: 4,
active_color: Color::new(255, 163, 72, 255),
inactive_color: Color::new(50, 50, 50, 255),
+ active_gradient: None,
+ inactive_gradient: None,
},
..Default::default()
};
diff --git a/niri-visual-tests/src/cases/tile.rs b/niri-visual-tests/src/cases/tile.rs
index ff0af227..2420314f 100644
--- a/niri-visual-tests/src/cases/tile.rs
+++ b/niri-visual-tests/src/cases/tile.rs
@@ -104,7 +104,13 @@ impl TestCase for Tile {
let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
self.tile
- .render(renderer, location, Scale::from(1.), true)
+ .render(
+ renderer,
+ location,
+ Scale::from(1.),
+ size.to_logical(1),
+ true,
+ )
.map(|elem| Box::new(elem) as _)
.collect()
}
diff --git a/resources/default-config.kdl b/resources/default-config.kdl
index dd4671fd..9cca2f8c 100644
--- a/resources/default-config.kdl
+++ b/resources/default-config.kdl
@@ -118,6 +118,24 @@ layout {
// Color of the ring on inactive monitors: red, green, blue, alpha.
inactive-color 80 80 80 255
+
+ // You can also use gradients. They take precedence over solid colors.
+ // Gradients are rendered the same as CSS linear-gradient(angle, from, to).
+ // Colors can be set in a variety of ways here:
+ // - CSS named colors: from="red"
+ // - RGB hex: from="#rgb", from="#rgba", from="#rrggbb", from="#rrggbbaa"
+ // - CSS-like notation: from="rgb(255, 127, 0)", rgba(), hsl() and a few others.
+ // The angle is the same as in linear-gradient, and is optional,
+ // defaulting to 180 (top-to-bottom gradient).
+ // You can use any CSS linear-gradient tool on the web to set these up.
+ //
+ // active-gradient from="#80c8ff" to="#bbddff" angle=45
+
+ // You can also color the gradient relative to the entire view
+ // of the workspace, rather than relative to just the window itself.
+ // To do that, set relative-to="workspace-view".
+ //
+ // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// You can also add a border. It's similar to the focus ring, but always visible.
@@ -129,6 +147,9 @@ layout {
width 4
active-color 255 200 127 255
inactive-color 80 80 80 255
+
+ // active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
+ // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
diff --git a/src/backend/tty.rs b/src/backend/tty.rs
index a0a50dd1..1f0cdb16 100644
--- a/src/backend/tty.rs
+++ b/src/backend/tty.rs
@@ -53,6 +53,7 @@ use super::RenderResult;
use crate::frame_clock::FrameClock;
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::renderer::AsGlesRenderer;
+use crate::render_helpers::shaders;
use crate::utils::get_monotonic_time;
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
@@ -443,6 +444,8 @@ impl Tty {
renderer.bind_wl_display(&niri.display_handle)?;
+ shaders::init(renderer.as_gles_renderer());
+
// Create the dmabuf global.
let primary_formats = renderer.dmabuf_formats().collect::<HashSet<_>>();
let default_feedback =
diff --git a/src/backend/winit.rs b/src/backend/winit.rs
index 7f35e153..e9413698 100644
--- a/src/backend/winit.rs
+++ b/src/backend/winit.rs
@@ -19,6 +19,7 @@ use smithay::reexports::winit::window::WindowBuilder;
use super::RenderResult;
use crate::niri::{Niri, RedrawState, State};
+use crate::render_helpers::shaders;
use crate::utils::get_monotonic_time;
pub struct Winit {
@@ -123,14 +124,13 @@ impl Winit {
}
pub fn init(&mut self, niri: &mut Niri) {
- if let Err(err) = self
- .backend
- .renderer()
- .bind_wl_display(&niri.display_handle)
- {
+ let renderer = self.backend.renderer();
+ if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
warn!("error binding renderer wl_display: {err}");
}
+ shaders::init(renderer);
+
niri.add_output(self.output.clone(), None);
}
diff --git a/src/layout/focus_ring.rs b/src/layout/focus_ring.rs
index 0addd204..24013510 100644
--- a/src/layout/focus_ring.rs
+++ b/src/layout/focus_ring.rs
@@ -1,26 +1,41 @@
use std::iter::zip;
use arrayvec::ArrayVec;
-use niri_config;
+use niri_config::{self, GradientRelativeTo};
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::Kind;
-use smithay::utils::{Logical, Point, Scale, Size};
+use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
+
+use crate::niri_render_elements;
+use crate::render_helpers::gradient::GradientRenderElement;
+use crate::render_helpers::renderer::NiriRenderer;
#[derive(Debug)]
pub struct FocusRing {
buffers: [SolidColorBuffer; 4],
locations: [Point<i32, Logical>; 4],
+ sizes: [Size<i32, Logical>; 4],
+ full_size: Size<i32, Logical>,
+ is_active: bool,
is_border: bool,
config: niri_config::FocusRing,
}
-pub type FocusRingRenderElement = SolidColorRenderElement;
+niri_render_elements! {
+ FocusRingRenderElement => {
+ SolidColor = SolidColorRenderElement,
+ Gradient = GradientRenderElement,
+ }
+}
impl FocusRing {
pub fn new(config: niri_config::FocusRing) -> Self {
Self {
buffers: Default::default(),
locations: Default::default(),
+ sizes: Default::default(),
+ full_size: Default::default(),
+ is_active: false,
is_border: false,
config,
}
@@ -32,20 +47,25 @@ impl FocusRing {
pub fn update(&mut self, win_size: Size<i32, Logical>, is_border: bool) {
let width = i32::from(self.config.width);
+ self.full_size = win_size + Size::from((width * 2, width * 2));
if is_border {
- self.buffers[0].resize((win_size.w + width * 2, width));
- self.buffers[1].resize((win_size.w + width * 2, width));
- self.buffers[2].resize((width, win_size.h));
- self.buffers[3].resize((width, win_size.h));
+ self.sizes[0] = Size::from((win_size.w + width * 2, width));
+ self.sizes[1] = Size::from((win_size.w + width * 2, width));
+ self.sizes[2] = Size::from((width, win_size.h));
+ self.sizes[3] = Size::from((width, win_size.h));
+
+ for (buf, size) in zip(&mut self.buffers, self.sizes) {
+ buf.resize(size);
+ }
self.locations[0] = Point::from((-width, -width));
self.locations[1] = Point::from((-width, win_size.h));
self.locations[2] = Point::from((-width, 0));
self.locations[3] = Point::from((win_size.w, 0));
} else {
- let size = win_size + Size::from((width * 2, width * 2));
- self.buffers[0].resize(size);
+ self.sizes[0] = self.full_size;
+ self.buffers[0].resize(self.sizes[0]);
self.locations[0] = Point::from((-width, -width));
}
@@ -62,12 +82,16 @@ impl FocusRing {
for buf in &mut self.buffers {
buf.set_color(color);
}
+
+ self.is_active = is_active;
}
- pub fn render(
+ pub fn render<R: NiriRenderer>(
&self,
+ renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
+ view_size: Size<i32, Logical>,
) -> impl Iterator<Item = FocusRingRenderElement> {
let mut rv = ArrayVec::<_, 4>::new();
@@ -75,23 +99,56 @@ impl FocusRing {
return rv.into_iter();
}
- let mut push = |buffer, location: Point<i32, Logical>| {
- let elem = SolidColorRenderElement::from_buffer(
- buffer,
- location.to_physical_precise_round(scale),
- scale,
- 1.,
- Kind::Unspecified,
- );
- rv.push(elem.into());
+ let gradient = if self.is_active {
+ self.config.active_gradient
+ } else {
+ self.config.inactive_gradient
+ };
+
+ let full_rect = Rectangle::from_loc_and_size(location + self.locations[0], self.full_size);
+ let view_rect = Rectangle::from_loc_and_size((0, 0), view_size);
+
+ let mut push = |buffer, location: Point<i32, Logical>, size: Size<i32, Logical>| {
+ let elem = gradient.and_then(|gradient| {
+ let gradient_area = match gradient.relative_to {
+ GradientRelativeTo::Window => full_rect,
+ GradientRelativeTo::WorkspaceView => view_rect,
+ };
+ GradientRenderElement::new(
+ renderer,
+ scale,
+ Rectangle::from_loc_and_size(location, size),
+ gradient_area,
+ gradient.from.into(),
+ gradient.to.into(),
+ ((gradient.angle as f32) - 90.).to_radians(),
+ )
+ .map(Into::into)
+ });
+
+ let elem = elem.unwrap_or_else(|| {
+ SolidColorRenderElement::from_buffer(
+ buffer,
+ location.to_physical_precise_round(scale),
+ scale,
+ 1.,
+ Kind::Unspecified,
+ )
+ .into()
+ });
+ rv.push(elem);
};
if self.is_border {
- for (buf, loc) in zip(&self.buffers, self.locations) {
- push(buf, location + loc);
+ for (buf, (loc, size)) in zip(&self.buffers, zip(self.locations, self.sizes)) {
+ push(buf, location + loc, size);
}
} else {
- push(&self.buffers[0], location + self.locations[0]);
+ push(
+ &self.buffers[0],
+ location + self.locations[0],
+ self.sizes[0],
+ );
}
rv.into_iter()
diff --git a/src/layout/tile.rs b/src/layout/tile.rs
index 28bcfb47..ca2779fe 100644
--- a/src/layout/tile.rs
+++ b/src/layout/tile.rs
@@ -7,7 +7,7 @@ use smithay::backend::renderer::element::utils::RescaleRenderElement;
use smithay::backend::renderer::element::{Element, Kind};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
-use super::focus_ring::FocusRing;
+use super::focus_ring::{FocusRing, FocusRingRenderElement};
use super::{LayoutElement, LayoutElementRenderElement, Options};
use crate::animation::Animation;
use crate::niri_render_elements;
@@ -51,6 +51,7 @@ pub struct Tile<W: LayoutElement> {
niri_render_elements! {
TileRenderElement<R> => {
LayoutElement = LayoutElementRenderElement<R>,
+ FocusRing = FocusRingRenderElement,
SolidColor = SolidColorRenderElement,
Offscreen = RescaleRenderElement<OffscreenRenderElement>,
}
@@ -297,6 +298,7 @@ impl<W: LayoutElement> Tile<W> {
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
+ view_size: Size<i32, Logical>,
focus_ring: bool,
) -> impl Iterator<Item = TileRenderElement<R>> {
let rv = self
@@ -307,12 +309,21 @@ impl<W: LayoutElement> Tile<W> {
let elem = self.effective_border_width().map(|width| {
self.border
- .render(location + Point::from((width, width)), scale)
+ .render(
+ renderer,
+ location + Point::from((width, width)),
+ scale,
+ view_size,
+ )
.map(Into::into)
});
let rv = rv.chain(elem.into_iter().flatten());
- let elem = focus_ring.then(|| self.focus_ring.render(location, scale).map(Into::into));
+ let elem = focus_ring.then(|| {
+ self.focus_ring
+ .render(renderer, location, scale, view_size)
+ .map(Into::into)
+ });
let rv = rv.chain(elem.into_iter().flatten());
let elem = self.is_fullscreen.then(|| {
@@ -333,11 +344,12 @@ impl<W: LayoutElement> Tile<W> {
renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
+ view_size: Size<i32, Logical>,
focus_ring: bool,
) -> impl Iterator<Item = TileRenderElement<R>> {
if let Some(anim) = &self.open_animation {
let renderer = renderer.as_gles_renderer();
- let elements = self.render_inner(renderer, location, scale, focus_ring);
+ let elements = self.render_inner(renderer, location, scale, view_size, focus_ring);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
let elem = OffscreenRenderElement::new(
@@ -365,7 +377,7 @@ impl<W: LayoutElement> Tile<W> {
} else {
self.window().set_offscreen_element_id(None);
- let elements = self.render_inner(renderer, location, scale, focus_ring);
+ let elements = self.render_inner(renderer, location, scale, view_size, focus_ring);
None.into_iter().chain(Some(elements).into_iter().flatten())
}
}
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index 5e872340..b9016098 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -1188,7 +1188,7 @@ impl<W: LayoutElement> Workspace<W> {
first = false;
rv.extend(
- tile.render(renderer, tile_pos, output_scale, focus_ring)
+ tile.render(renderer, tile_pos, output_scale, self.view_size, focus_ring)
.map(Into::into),
);
}
diff --git a/src/render_helpers/gradient.rs b/src/render_helpers/gradient.rs
new file mode 100644
index 00000000..1dbed05a
--- /dev/null
+++ b/src/render_helpers/gradient.rs
@@ -0,0 +1,143 @@
+use std::f32::consts::{self, FRAC_PI_2, PI};
+
+use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
+use smithay::backend::renderer::gles::element::PixelShaderElement;
+use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
+use smithay::backend::renderer::utils::CommitCounter;
+use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
+
+use super::primary_gpu_pixel_shader::PrimaryGpuPixelShaderRenderElement;
+use super::renderer::NiriRenderer;
+use super::shaders::Shaders;
+use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
+
+/// Renders a sub- or super-rect of an angled linear gradient like CSS linear-gradient(angle, a, b).
+#[derive(Debug)]
+pub struct GradientRenderElement(PrimaryGpuPixelShaderRenderElement);
+
+impl GradientRenderElement {
+ pub fn new(
+ renderer: &mut impl NiriRenderer,
+ scale: Scale<f64>,
+ area: Rectangle<i32, Logical>,
+ gradient_area: Rectangle<i32, Logical>,
+ color_from: [f32; 4],
+ color_to: [f32; 4],
+ mut angle: f32,
+ ) -> Option<Self> {
+ let shader = Shaders::get(renderer).gradient_border.clone()?;
+ let g_offset = (area.loc - gradient_area.loc).to_f64().to_physical(scale);
+
+ let g_size = gradient_area.size.to_f64().to_physical(scale);
+ let (w, h) = (g_size.w as f32, g_size.h as f32);
+ let g_area_angle = f32::atan2(h, w);
+ let g_area_diag = f32::hypot(h, w);
+
+ // Normalize the angle to [0°; 360°).
+ while angle < 0. {
+ angle += consts::TAU;
+ }
+ while angle >= consts::TAU {
+ angle -= consts::TAU;
+ }
+
+ let angle_diag_to_grad =
+ if (0. ..=FRAC_PI_2).contains(&angle) || (PI..=PI + FRAC_PI_2).contains(&angle) {
+ angle - g_area_angle
+ } else {
+ (PI - angle) - g_area_angle
+ };
+ let g_total = angle_diag_to_grad.cos().abs() * g_area_diag;
+
+ let elem = PixelShaderElement::new(
+ shader,
+ area,
+ None,
+ 1.,
+ vec![
+ Uniform::new("color_from", color_from),
+ Uniform::new("color_to", color_to),
+ Uniform::new("angle", angle),
+ Uniform::new("gradient_offset", (g_offset.x as f32, g_offset.y as f32)),
+ Uniform::new("gradient_width", w),
+ Uniform::new("gradient_total", g_total),
+ ],
+ Kind::Unspecified,
+ );
+ Some(Self(PrimaryGpuPixelShaderRenderElement(elem)))
+ }
+}
+
+impl Element for GradientRenderElement {
+ fn id(&self) -> &Id {
+ self.0.id()
+ }
+
+ fn current_commit(&self) -> CommitCounter {
+ self.0.current_commit()
+ }
+
+ fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
+ self.0.geometry(scale)
+ }
+
+ fn transform(&self) -> Transform {
+ self.0.transform()
+ }
+
+ fn src(&self) -> Rectangle<f64, Buffer> {
+ self.0.src()
+ }
+
+ fn damage_since(
+ &self,
+ scale: Scale<f64>,
+ commit: Option<CommitCounter>,
+ ) -> Vec<Rectangle<i32, Physical>> {
+ self.0.damage_since(scale, commit)
+ }
+
+ fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
+ self.0.opaque_regions(scale)
+ }
+
+ fn alpha(&self) -> f32 {
+ self.0.alpha()
+ }
+
+ fn kind(&self) -> Kind {
+ self.0.kind()
+ }
+}
+
+impl RenderElement<GlesRenderer> for GradientRenderElement {
+ fn draw(
+ &self,
+ frame: &mut GlesFrame<'_>,
+ src: Rectangle<f64, Buffer>,
+ dst: Rectangle<i32, Physical>,
+ damage: &[Rectangle<i32, Physical>],
+ ) -> Result<(), GlesError> {
+ RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage)
+ }
+
+ fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
+ self.0.underlying_storage(renderer)
+ }
+}
+
+impl<'render> RenderElement<TtyRenderer<'render>> for GradientRenderElement {
+ fn draw(
+ &self,
+ frame: &mut TtyFrame<'_, '_>,
+ src: Rectangle<f64, Buffer>,
+ dst: Rectangle<i32, Physical>,
+ damage: &[Rectangle<i32, Physical>],
+ ) -> Result<(), TtyRendererError<'render>> {
+ RenderElement::<TtyRenderer<'_>>::draw(&self.0, frame, src, dst, damage)
+ }
+
+ fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
+ self.0.underlying_storage(renderer)
+ }
+}
diff --git a/src/render_helpers/mod.rs b/src/render_helpers/mod.rs
index c05877f9..7d7ea9c1 100644
--- a/src/render_helpers/mod.rs
+++ b/src/render_helpers/mod.rs
@@ -6,10 +6,13 @@ use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::{Bind, ExportMem, Frame, Offscreen, Renderer};
use smithay::utils::{Physical, Rectangle, Scale, Size, Transform};
+pub mod gradient;
pub mod offscreen;
+pub mod primary_gpu_pixel_shader;
pub mod primary_gpu_texture;
pub mod render_elements;
pub mod renderer;
+pub mod shaders;
pub fn render_to_texture(
renderer: &mut GlesRenderer,
diff --git a/src/render_helpers/primary_gpu_pixel_shader.rs b/src/render_helpers/primary_gpu_pixel_shader.rs
new file mode 100644
index 00000000..b3b75023
--- /dev/null
+++ b/src/render_helpers/primary_gpu_pixel_shader.rs
@@ -0,0 +1,97 @@
+use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
+use smithay::backend::renderer::gles::element::PixelShaderElement;
+use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
+use smithay::backend::renderer::utils::CommitCounter;
+use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
+
+use super::renderer::AsGlesFrame;
+use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
+
+/// Wrapper for a poxel shader from the primary GPU for rendering with the primary GPU.
+#[derive(Debug)]
+pub struct PrimaryGpuPixelShaderRenderElement(pub PixelShaderElement);
+
+impl Element for PrimaryGpuPixelShaderRenderElement {
+ fn id(&self) -> &Id {
+ self.0.id()
+ }
+
+ fn current_commit(&self) -> CommitCounter {
+ self.0.current_commit()
+ }
+
+ fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
+ self.0.geometry(scale)
+ }
+
+ fn transform(&self) -> Transform {
+ self.0.transform()
+ }
+
+ fn src(&self) -> Rectangle<f64, Buffer> {
+ self.0.src()
+ }
+
+ fn damage_since(
+ &self,
+ scale: Scale<f64>,
+ commit: Option<CommitCounter>,
+ ) -> Vec<Rectangle<i32, Physical>> {
+ self.0.damage_since(scale, commit)
+ }
+
+ fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
+ self.0.opaque_regions(scale)
+ }
+
+ fn alpha(&self) -> f32 {
+ self.0.alpha()
+ }
+
+ fn kind(&self) -> Kind {
+ self.0.kind()
+ }
+}
+
+impl RenderElement<GlesRenderer> for PrimaryGpuPixelShaderRenderElement {
+ fn draw(
+ &self,
+ frame: &mut GlesFrame<'_>,
+ src: Rectangle<f64, Buffer>,
+ dst: Rectangle<i32, Physical>,
+ damage: &[Rectangle<i32, Physical>],
+ ) -> Result<(), GlesError> {
+ let gles_frame = frame.as_gles_frame();
+ RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
+ Ok(())
+ }
+
+ fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
+ // If scanout for things other than Wayland buffers is implemented, this will need to take
+ // the target GPU into account.
+ None
+ }
+}
+
+impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuPixelShaderRenderElement {
+ fn draw(
+ &self,
+ frame: &mut TtyFrame<'_, '_>,
+ src: Rectangle<f64, Buffer>,
+ dst: Rectangle<i32, Physical>,
+ damage: &[Rectangle<i32, Physical>],
+ ) -> Result<(), TtyRendererError<'render>> {
+ let gles_frame = frame.as_gles_frame();
+ RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
+ Ok(())
+ }
+
+ fn underlying_storage(