Skip to content

Commit 3ed9547

Browse files
authored
Draw rounded rectangles using shader (#2463)
1 parent f3382c1 commit 3ed9547

File tree

7 files changed

+187
-93
lines changed

7 files changed

+187
-93
lines changed

data/gala.gresource.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<gresource prefix="/io/elementary/desktop/gala">
2424
<file compressed="true">shaders/colorblindness-correction.vert</file>
2525
<file compressed="true">shaders/monochrome.vert</file>
26+
<file compressed="true">shaders/rounded-corners.vert</file>
2627
</gresource>
2728
<gresource prefix="/io/elementary/desktop/gala-daemon">
2829
<file compressed="true">gala-daemon.css</file>

data/shaders/rounded-corners.vert

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2025 elementary, Inc. <https://elementary.io>
3+
* SPDX-License-Identifier: LGPL-3.0-or-later
4+
*/
5+
6+
// based on shader from mutter
7+
8+
uniform sampler2D tex;
9+
uniform float clip_radius;
10+
uniform vec2 actor_size;
11+
12+
float rounded_rect_coverage (vec2 p) {
13+
float center_left = clip_radius + 1.5;
14+
float center_right = actor_size.x - clip_radius - 0.55;
15+
float center_x;
16+
17+
if (p.x < center_left)
18+
center_x = center_left;
19+
else if (p.x >= center_right)
20+
center_x = center_right;
21+
else
22+
return 1.0;
23+
24+
float center_top = clip_radius + 1.5;
25+
float center_bottom = actor_size.y - clip_radius - 0.55;
26+
float center_y;
27+
28+
if (p.y < center_top)
29+
center_y = center_top;
30+
else if (p.y > center_bottom)
31+
center_y = center_bottom;
32+
else
33+
return 1.0;
34+
35+
vec2 delta = p - vec2 (center_x, center_y);
36+
float dist_squared = dot (delta, delta);
37+
38+
// Fully outside the circle
39+
float outer_radius = clip_radius + 0.5;
40+
if (dist_squared > (outer_radius * outer_radius))
41+
return 0.0;
42+
43+
// Fully inside the circle
44+
float inner_radius = clip_radius - 0.5;
45+
if (dist_squared <= (inner_radius * inner_radius))
46+
return 1.0;
47+
48+
// Only pixels on the edge of the curve need expensive antialiasing
49+
return smoothstep (outer_radius, inner_radius, sqrt (dist_squared));
50+
}
51+
52+
void main () {
53+
vec4 sample = texture2D (tex, cogl_tex_coord0_in.xy);
54+
55+
vec2 texture_coord = cogl_tex_coord0_in.xy * actor_size;
56+
float res = rounded_rect_coverage (texture_coord);
57+
58+
cogl_color_out = sample * cogl_color_in * res;
59+
}

lib/RoundedCornersEffect.vala

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2025 elementary, Inc. <https://elementary.io>
3+
* SPDX-License-Identifier: LGPL-3.0-or-later
4+
*/
5+
6+
public class Gala.RoundedCornersEffect : Clutter.ShaderEffect {
7+
private const int CLIP_RADIUS_OFFSET = 3;
8+
9+
public float clip_radius {
10+
construct set {
11+
set_uniform_value ("clip_radius", value + CLIP_RADIUS_OFFSET);
12+
}
13+
}
14+
15+
private float _monitor_scale = 1.0f;
16+
public float monitor_scale {
17+
get {
18+
return _monitor_scale;
19+
}
20+
construct set {
21+
_monitor_scale = value;
22+
23+
if (actor != null) {
24+
update_actor_size ();
25+
}
26+
}
27+
}
28+
29+
public RoundedCornersEffect (float clip_radius, float monitor_scale) {
30+
Object (
31+
#if HAS_MUTTER48
32+
shader_type: Cogl.ShaderType.FRAGMENT,
33+
#else
34+
shader_type: Clutter.ShaderType.FRAGMENT_SHADER,
35+
#endif
36+
clip_radius: clip_radius,
37+
monitor_scale: monitor_scale
38+
);
39+
}
40+
41+
construct {
42+
try {
43+
var bytes = GLib.resources_lookup_data ("/io/elementary/desktop/gala/shaders/rounded-corners.vert", GLib.ResourceLookupFlags.NONE);
44+
set_shader_source ((string) bytes.get_data ());
45+
} catch (Error e) {
46+
critical ("Unable to load rounded-corners.vert: %s", e.message);
47+
}
48+
}
49+
50+
public override void set_actor (Clutter.Actor? new_actor) {
51+
if (actor != null) {
52+
actor.notify["width"].disconnect (update_actor_size);
53+
actor.notify["height"].disconnect (update_actor_size);
54+
}
55+
56+
base.set_actor (new_actor);
57+
58+
if (actor != null) {
59+
actor.notify["width"].connect (update_actor_size);
60+
actor.notify["height"].connect (update_actor_size);
61+
62+
update_actor_size ();
63+
}
64+
}
65+
66+
private void update_actor_size () requires (actor != null) {
67+
float[] actor_size = {
68+
actor.width * monitor_scale,
69+
actor.height * monitor_scale
70+
};
71+
72+
var actor_size_value = GLib.Value (typeof (Clutter.ShaderFloat));
73+
Clutter.Value.set_shader_float (actor_size_value, actor_size);
74+
set_uniform_value ("actor_size", actor_size_value);
75+
}
76+
}

lib/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ gala_lib_sources = files(
1515
'Drawing/Utilities.vala',
1616
'Image.vala',
1717
'Plugin.vala',
18+
'RoundedCornersEffect.vala',
1819
'ShadowEffect.vala',
1920
'Utils.vala',
2021
'WindowIcon.vala',

src/Widgets/MultitaskingView/Tooltip.vala

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
/**
88
* Clutter actor to display text in a tooltip-like component.
99
*/
10-
public class Gala.Tooltip : CanvasActor {
10+
public class Gala.Tooltip : Clutter.Actor {
1111
/**
1212
* Actor to display the Tooltip text.
1313
*/
@@ -37,31 +37,17 @@ public class Gala.Tooltip : CanvasActor {
3737
add_child (text_actor);
3838

3939
layout_manager = new Clutter.BinLayout ();
40+
background_color = {
41+
(uint8) (Drawing.Color.TOOLTIP_BACKGROUND.red * uint8.MAX),
42+
(uint8) (Drawing.Color.TOOLTIP_BACKGROUND.green * uint8.MAX),
43+
(uint8) (Drawing.Color.TOOLTIP_BACKGROUND.blue * uint8.MAX),
44+
(uint8) (Drawing.Color.TOOLTIP_BACKGROUND.alpha * uint8.MAX)
45+
};
46+
47+
add_effect (new RoundedCornersEffect (3, 1.0f));
4048
}
4149

4250
public void set_text (string new_text) {
4351
text_actor.text = new_text;
4452
}
45-
46-
protected override void draw (Cairo.Context ctx, int width, int height) {
47-
ctx.save ();
48-
ctx.set_operator (Cairo.Operator.CLEAR);
49-
ctx.paint ();
50-
ctx.clip ();
51-
ctx.reset_clip ();
52-
ctx.set_operator (Cairo.Operator.OVER);
53-
54-
var background_color = Drawing.Color.TOOLTIP_BACKGROUND;
55-
ctx.set_source_rgba (
56-
background_color.red,
57-
background_color.green,
58-
background_color.blue,
59-
background_color.alpha
60-
);
61-
62-
Drawing.Utilities.cairo_rounded_rectangle (ctx, 0, 0, width, height, 4);
63-
ctx.fill ();
64-
65-
ctx.restore ();
66-
}
6753
}

src/Widgets/MultitaskingView/WindowClone.vala

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public class Gala.WindowClone : ActorTarget, RootTarget {
4141
*/
4242
public bool active {
4343
set {
44+
active_shape.update_color ();
45+
4446
active_shape.save_easing_state ();
4547
active_shape.set_easing_duration (Utils.get_animation_duration (FADE_ANIMATION_DURATION));
4648
active_shape.opacity = value ? 255 : 0;
@@ -687,33 +689,22 @@ public class Gala.WindowClone : ActorTarget, RootTarget {
687689
/**
688690
* Border to show around the selected window when using keyboard navigation.
689691
*/
690-
private class ActiveShape : CanvasActor {
692+
private class ActiveShape : Clutter.Actor {
691693
private const int BORDER_RADIUS = 16;
692694
private const double COLOR_OPACITY = 0.8;
693695

694696
construct {
695-
notify["opacity"].connect (invalidate);
696-
}
697-
698-
public void invalidate () {
699-
content.invalidate ();
697+
add_effect (new RoundedCornersEffect (BORDER_RADIUS, 1.0f));
700698
}
701699

702-
protected override void draw (Cairo.Context cr, int width, int height) {
703-
if (!visible || opacity == 0) {
704-
return;
705-
}
706-
707-
var color = Drawing.StyleManager.get_instance ().theme_accent_color;
708-
709-
cr.save ();
710-
cr.set_operator (Cairo.Operator.CLEAR);
711-
cr.paint ();
712-
cr.restore ();
713-
714-
Drawing.Utilities.cairo_rounded_rectangle (cr, 0, 0, width, height, BORDER_RADIUS);
715-
cr.set_source_rgba (color.red, color.green, color.blue, COLOR_OPACITY);
716-
cr.fill ();
700+
public void update_color () {
701+
var accent_color = Drawing.StyleManager.get_instance ().theme_accent_color;
702+
background_color = {
703+
(uint8) (accent_color.red * uint8.MAX),
704+
(uint8) (accent_color.green * uint8.MAX),
705+
(uint8) (accent_color.blue * uint8.MAX),
706+
(uint8) (COLOR_OPACITY * uint8.MAX)
707+
};
717708
}
718709
}
719710
}

src/Widgets/WindowSwitcher/WindowSwitcherIcon.vala

Lines changed: 29 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,46 @@
33
* SPDX-License-Identifier: GPL-3.0-or-later
44
*/
55

6-
public class Gala.WindowSwitcherIcon : CanvasActor {
6+
public class Gala.WindowSwitcherIcon : Clutter.Actor {
77
private const int WRAPPER_BORDER_RADIUS = 3;
88

99
public Meta.Window window { get; construct; }
1010

1111
private WindowIcon icon;
12+
private RoundedCornersEffect rounded_corners_effect;
1213

13-
private bool _selected = false;
1414
public bool selected {
15-
get {
16-
return _selected;
17-
}
1815
set {
19-
_selected = value;
20-
content.invalidate ();
16+
if (value) {
17+
var accent_color = Drawing.StyleManager.get_instance ().theme_accent_color;
18+
background_color = {
19+
(uint8) (accent_color.red * uint8.MAX),
20+
(uint8) (accent_color.green * uint8.MAX),
21+
(uint8) (accent_color.blue * uint8.MAX),
22+
(uint8) (accent_color.alpha * uint8.MAX)
23+
};
24+
} else {
25+
#if HAS_MUTTER47
26+
background_color = Cogl.Color.from_4f (0, 0, 0, 0);
27+
#else
28+
background_color = Clutter.Color.alloc ();
29+
#endif
30+
}
31+
32+
get_accessible ().notify_state_change (Atk.StateType.SELECTED, value);
33+
get_accessible ().notify_state_change (Atk.StateType.FOCUSED, value);
2134
}
2235
}
2336

24-
private float _scale_factor = 1.0f;
2537
public float scale_factor {
26-
get {
27-
return _scale_factor;
28-
}
2938
set {
30-
_scale_factor = value;
39+
var indicator_size = Utils.scale_to_int (
40+
(WindowSwitcher.ICON_SIZE + WindowSwitcher.WRAPPER_PADDING * 2),
41+
value
42+
);
43+
set_size (indicator_size, indicator_size);
3144

32-
update_size ();
45+
rounded_corners_effect.monitor_scale = value;
3346
}
3447
}
3548

@@ -40,6 +53,9 @@ public class Gala.WindowSwitcherIcon : CanvasActor {
4053
icon.add_constraint (new Clutter.AlignConstraint (this, Clutter.AlignAxis.BOTH, 0.5f));
4154
add_child (icon);
4255

56+
rounded_corners_effect = new RoundedCornersEffect (WRAPPER_BORDER_RADIUS, scale_factor);
57+
add_effect (rounded_corners_effect);
58+
4359
get_accessible ().accessible_name = window.title;
4460
get_accessible ().accessible_role = LIST_ITEM;
4561
get_accessible ().notify_state_change (Atk.StateType.FOCUSABLE, true);
@@ -48,40 +64,4 @@ public class Gala.WindowSwitcherIcon : CanvasActor {
4864

4965
this.scale_factor = scale_factor;
5066
}
51-
52-
private void update_size () {
53-
var indicator_size = Utils.scale_to_int (
54-
(WindowSwitcher.ICON_SIZE + WindowSwitcher.WRAPPER_PADDING * 2),
55-
scale_factor
56-
);
57-
set_size (indicator_size, indicator_size);
58-
}
59-
60-
protected override void draw (Cairo.Context ctx, int width, int height) {
61-
ctx.save ();
62-
ctx.set_operator (Cairo.Operator.CLEAR);
63-
ctx.paint ();
64-
ctx.clip ();
65-
ctx.reset_clip ();
66-
67-
if (selected) {
68-
// draw rect
69-
var rgba = Drawing.StyleManager.get_instance ().theme_accent_color;
70-
ctx.set_source_rgba (
71-
rgba.red,
72-
rgba.green,
73-
rgba.blue,
74-
rgba.alpha
75-
);
76-
var rect_radius = Utils.scale_to_int (WRAPPER_BORDER_RADIUS, scale_factor);
77-
Drawing.Utilities.cairo_rounded_rectangle (ctx, 0, 0, width, height, rect_radius);
78-
ctx.set_operator (Cairo.Operator.SOURCE);
79-
ctx.fill ();
80-
81-
ctx.restore ();
82-
}
83-
84-
get_accessible ().notify_state_change (Atk.StateType.SELECTED, selected);
85-
get_accessible ().notify_state_change (Atk.StateType.FOCUSED, selected);
86-
}
8767
}

0 commit comments

Comments
 (0)