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); - } }