diff --git a/data/gala.gresource.xml b/data/gala.gresource.xml
index a50d83f0b..c12dbbf02 100644
--- a/data/gala.gresource.xml
+++ b/data/gala.gresource.xml
@@ -23,6 +23,7 @@
shaders/colorblindness-correction.vert
shaders/monochrome.vert
+ shaders/rounded-corners.vert
gala-daemon.css
diff --git a/data/shaders/rounded-corners.vert b/data/shaders/rounded-corners.vert
new file mode 100644
index 000000000..b83a016f9
--- /dev/null
+++ b/data/shaders/rounded-corners.vert
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2025 elementary, Inc.
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+// based on shader from mutter
+
+uniform sampler2D tex;
+uniform float clip_radius;
+uniform vec2 actor_size;
+
+float rounded_rect_coverage (vec2 p) {
+ float center_left = clip_radius + 1.5;
+ float center_right = actor_size.x - clip_radius - 0.55;
+ float center_x;
+
+ if (p.x < center_left)
+ center_x = center_left;
+ else if (p.x >= center_right)
+ center_x = center_right;
+ else
+ return 1.0;
+
+ float center_top = clip_radius + 1.5;
+ float center_bottom = actor_size.y - clip_radius - 0.55;
+ float center_y;
+
+ if (p.y < center_top)
+ center_y = center_top;
+ else if (p.y > center_bottom)
+ center_y = center_bottom;
+ else
+ return 1.0;
+
+ vec2 delta = p - vec2 (center_x, center_y);
+ float dist_squared = dot (delta, delta);
+
+ // Fully outside the circle
+ float outer_radius = clip_radius + 0.5;
+ if (dist_squared > (outer_radius * outer_radius))
+ return 0.0;
+
+ // Fully inside the circle
+ float inner_radius = clip_radius - 0.5;
+ if (dist_squared <= (inner_radius * inner_radius))
+ return 1.0;
+
+ // Only pixels on the edge of the curve need expensive antialiasing
+ return smoothstep (outer_radius, inner_radius, sqrt (dist_squared));
+}
+
+void main () {
+ vec4 sample = texture2D (tex, cogl_tex_coord0_in.xy);
+
+ vec2 texture_coord = cogl_tex_coord0_in.xy * actor_size;
+ float res = rounded_rect_coverage (texture_coord);
+
+ cogl_color_out = sample * cogl_color_in * res;
+}
diff --git a/lib/RoundedCornersEffect.vala b/lib/RoundedCornersEffect.vala
new file mode 100644
index 000000000..ba9369d46
--- /dev/null
+++ b/lib/RoundedCornersEffect.vala
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2025 elementary, Inc.
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+public class Gala.RoundedCornersEffect : Clutter.ShaderEffect {
+ private const int CLIP_RADIUS_OFFSET = 3;
+
+ public float clip_radius {
+ construct set {
+ set_uniform_value ("clip_radius", value + CLIP_RADIUS_OFFSET);
+ }
+ }
+
+ private float _monitor_scale = 1.0f;
+ public float monitor_scale {
+ get {
+ return _monitor_scale;
+ }
+ construct set {
+ _monitor_scale = value;
+
+ if (actor != null) {
+ update_actor_size ();
+ }
+ }
+ }
+
+ public RoundedCornersEffect (float clip_radius, float monitor_scale) {
+ Object (
+#if HAS_MUTTER48
+ shader_type: Cogl.ShaderType.FRAGMENT,
+#else
+ shader_type: Clutter.ShaderType.FRAGMENT_SHADER,
+#endif
+ clip_radius: clip_radius,
+ monitor_scale: monitor_scale
+ );
+ }
+
+ construct {
+ try {
+ var bytes = GLib.resources_lookup_data ("/io/elementary/desktop/gala/shaders/rounded-corners.vert", GLib.ResourceLookupFlags.NONE);
+ set_shader_source ((string) bytes.get_data ());
+ } catch (Error e) {
+ critical ("Unable to load rounded-corners.vert: %s", e.message);
+ }
+ }
+
+ public override void set_actor (Clutter.Actor? new_actor) {
+ if (actor != null) {
+ actor.notify["width"].disconnect (update_actor_size);
+ actor.notify["height"].disconnect (update_actor_size);
+ }
+
+ base.set_actor (new_actor);
+
+ if (actor != null) {
+ actor.notify["width"].connect (update_actor_size);
+ actor.notify["height"].connect (update_actor_size);
+
+ update_actor_size ();
+ }
+ }
+
+ private void update_actor_size () requires (actor != null) {
+ float[] actor_size = {
+ actor.width * monitor_scale,
+ actor.height * monitor_scale
+ };
+
+ var actor_size_value = GLib.Value (typeof (Clutter.ShaderFloat));
+ Clutter.Value.set_shader_float (actor_size_value, actor_size);
+ set_uniform_value ("actor_size", actor_size_value);
+ }
+}
diff --git a/lib/meson.build b/lib/meson.build
index 640147a2b..d94c825c1 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -15,6 +15,7 @@ gala_lib_sources = files(
'Drawing/Utilities.vala',
'Image.vala',
'Plugin.vala',
+ 'RoundedCornersEffect.vala',
'ShadowEffect.vala',
'Utils.vala',
'WindowIcon.vala',
diff --git a/src/Widgets/MultitaskingView/Tooltip.vala b/src/Widgets/MultitaskingView/Tooltip.vala
index 6a552a849..c5010e339 100644
--- a/src/Widgets/MultitaskingView/Tooltip.vala
+++ b/src/Widgets/MultitaskingView/Tooltip.vala
@@ -7,7 +7,7 @@
/**
* Clutter actor to display text in a tooltip-like component.
*/
-public class Gala.Tooltip : CanvasActor {
+public class Gala.Tooltip : Clutter.Actor {
/**
* Actor to display the Tooltip text.
*/
@@ -37,31 +37,17 @@ public class Gala.Tooltip : CanvasActor {
add_child (text_actor);
layout_manager = new Clutter.BinLayout ();
+ background_color = {
+ (uint8) (Drawing.Color.TOOLTIP_BACKGROUND.red * uint8.MAX),
+ (uint8) (Drawing.Color.TOOLTIP_BACKGROUND.green * uint8.MAX),
+ (uint8) (Drawing.Color.TOOLTIP_BACKGROUND.blue * uint8.MAX),
+ (uint8) (Drawing.Color.TOOLTIP_BACKGROUND.alpha * uint8.MAX)
+ };
+
+ add_effect (new RoundedCornersEffect (3, 1.0f));
}
public void set_text (string new_text) {
text_actor.text = new_text;
}
-
- protected override void draw (Cairo.Context ctx, int width, int height) {
- ctx.save ();
- ctx.set_operator (Cairo.Operator.CLEAR);
- ctx.paint ();
- ctx.clip ();
- ctx.reset_clip ();
- ctx.set_operator (Cairo.Operator.OVER);
-
- var background_color = Drawing.Color.TOOLTIP_BACKGROUND;
- ctx.set_source_rgba (
- background_color.red,
- background_color.green,
- background_color.blue,
- background_color.alpha
- );
-
- Drawing.Utilities.cairo_rounded_rectangle (ctx, 0, 0, width, height, 4);
- ctx.fill ();
-
- ctx.restore ();
- }
}
diff --git a/src/Widgets/MultitaskingView/WindowClone.vala b/src/Widgets/MultitaskingView/WindowClone.vala
index 6abc4f474..54723e037 100644
--- a/src/Widgets/MultitaskingView/WindowClone.vala
+++ b/src/Widgets/MultitaskingView/WindowClone.vala
@@ -41,6 +41,8 @@ public class Gala.WindowClone : ActorTarget, RootTarget {
*/
public bool active {
set {
+ active_shape.update_color ();
+
active_shape.save_easing_state ();
active_shape.set_easing_duration (Utils.get_animation_duration (FADE_ANIMATION_DURATION));
active_shape.opacity = value ? 255 : 0;
@@ -687,33 +689,22 @@ public class Gala.WindowClone : ActorTarget, RootTarget {
/**
* Border to show around the selected window when using keyboard navigation.
*/
- private class ActiveShape : CanvasActor {
+ private class ActiveShape : Clutter.Actor {
private const int BORDER_RADIUS = 16;
private const double COLOR_OPACITY = 0.8;
construct {
- notify["opacity"].connect (invalidate);
- }
-
- public void invalidate () {
- content.invalidate ();
+ add_effect (new RoundedCornersEffect (BORDER_RADIUS, 1.0f));
}
- protected override void draw (Cairo.Context cr, int width, int height) {
- if (!visible || opacity == 0) {
- return;
- }
-
- var color = Drawing.StyleManager.get_instance ().theme_accent_color;
-
- cr.save ();
- cr.set_operator (Cairo.Operator.CLEAR);
- cr.paint ();
- cr.restore ();
-
- Drawing.Utilities.cairo_rounded_rectangle (cr, 0, 0, width, height, BORDER_RADIUS);
- cr.set_source_rgba (color.red, color.green, color.blue, COLOR_OPACITY);
- cr.fill ();
+ public void update_color () {
+ var accent_color = Drawing.StyleManager.get_instance ().theme_accent_color;
+ background_color = {
+ (uint8) (accent_color.red * uint8.MAX),
+ (uint8) (accent_color.green * uint8.MAX),
+ (uint8) (accent_color.blue * uint8.MAX),
+ (uint8) (COLOR_OPACITY * uint8.MAX)
+ };
}
}
}
diff --git a/src/Widgets/WindowSwitcher/WindowSwitcherIcon.vala b/src/Widgets/WindowSwitcher/WindowSwitcherIcon.vala
index 6bb07754a..ff8167a77 100644
--- a/src/Widgets/WindowSwitcher/WindowSwitcherIcon.vala
+++ b/src/Widgets/WindowSwitcher/WindowSwitcherIcon.vala
@@ -3,33 +3,46 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-public class Gala.WindowSwitcherIcon : CanvasActor {
+public class Gala.WindowSwitcherIcon : Clutter.Actor {
private const int WRAPPER_BORDER_RADIUS = 3;
public Meta.Window window { get; construct; }
private WindowIcon icon;
+ private RoundedCornersEffect rounded_corners_effect;
- private bool _selected = false;
public bool selected {
- get {
- return _selected;
- }
set {
- _selected = value;
- content.invalidate ();
+ if (value) {
+ var accent_color = Drawing.StyleManager.get_instance ().theme_accent_color;
+ background_color = {
+ (uint8) (accent_color.red * uint8.MAX),
+ (uint8) (accent_color.green * uint8.MAX),
+ (uint8) (accent_color.blue * uint8.MAX),
+ (uint8) (accent_color.alpha * uint8.MAX)
+ };
+ } else {
+#if HAS_MUTTER47
+ background_color = Cogl.Color.from_4f (0, 0, 0, 0);
+#else
+ background_color = Clutter.Color.alloc ();
+#endif
+ }
+
+ get_accessible ().notify_state_change (Atk.StateType.SELECTED, value);
+ get_accessible ().notify_state_change (Atk.StateType.FOCUSED, value);
}
}
- private float _scale_factor = 1.0f;
public float scale_factor {
- get {
- return _scale_factor;
- }
set {
- _scale_factor = value;
+ var indicator_size = Utils.scale_to_int (
+ (WindowSwitcher.ICON_SIZE + WindowSwitcher.WRAPPER_PADDING * 2),
+ value
+ );
+ set_size (indicator_size, indicator_size);
- update_size ();
+ rounded_corners_effect.monitor_scale = value;
}
}
@@ -40,6 +53,9 @@ public class Gala.WindowSwitcherIcon : CanvasActor {
icon.add_constraint (new Clutter.AlignConstraint (this, Clutter.AlignAxis.BOTH, 0.5f));
add_child (icon);
+ rounded_corners_effect = new RoundedCornersEffect (WRAPPER_BORDER_RADIUS, scale_factor);
+ add_effect (rounded_corners_effect);
+
get_accessible ().accessible_name = window.title;
get_accessible ().accessible_role = LIST_ITEM;
get_accessible ().notify_state_change (Atk.StateType.FOCUSABLE, true);
@@ -48,40 +64,4 @@ public class Gala.WindowSwitcherIcon : CanvasActor {
this.scale_factor = scale_factor;
}
-
- private void update_size () {
- var indicator_size = Utils.scale_to_int (
- (WindowSwitcher.ICON_SIZE + WindowSwitcher.WRAPPER_PADDING * 2),
- scale_factor
- );
- set_size (indicator_size, indicator_size);
- }
-
- protected override void draw (Cairo.Context ctx, int width, int height) {
- ctx.save ();
- ctx.set_operator (Cairo.Operator.CLEAR);
- ctx.paint ();
- ctx.clip ();
- ctx.reset_clip ();
-
- if (selected) {
- // draw rect
- var rgba = Drawing.StyleManager.get_instance ().theme_accent_color;
- ctx.set_source_rgba (
- rgba.red,
- rgba.green,
- rgba.blue,
- rgba.alpha
- );
- var rect_radius = Utils.scale_to_int (WRAPPER_BORDER_RADIUS, scale_factor);
- Drawing.Utilities.cairo_rounded_rectangle (ctx, 0, 0, width, height, rect_radius);
- ctx.set_operator (Cairo.Operator.SOURCE);
- ctx.fill ();
-
- ctx.restore ();
- }
-
- get_accessible ().notify_state_change (Atk.StateType.SELECTED, selected);
- get_accessible ().notify_state_change (Atk.StateType.FOCUSED, selected);
- }
}