Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 11 additions & 55 deletions lib/CloseButton.vala
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ public class Gala.CloseButton : Clutter.Actor {

public float monitor_scale { get; construct set; }

// used to avoid changing hitbox of the button
private Clutter.Actor pixbuf_actor;
private Icon icon;
private Clutter.ClickAction click_action;

static construct {
Expand All @@ -26,18 +25,18 @@ public class Gala.CloseButton : Clutter.Actor {
construct {
reactive = true;

pixbuf_actor = new Clutter.Actor () {
icon = new Icon.from_resource (
Utils.BUTTON_SIZE, monitor_scale,
"/org/pantheon/desktop/gala/buttons/close.svg"
) {
pivot_point = { 0.5f, 0.5f }
};
add_child (pixbuf_actor);
add_child (icon);

click_action = new Clutter.ClickAction ();
add_action (click_action);
click_action.clicked.connect (on_clicked);
click_action.notify["pressed"].connect (on_pressed_changed);

load_pixbuf ();
notify["monitor-scale"].connect (load_pixbuf);
}

private void on_clicked () {
Expand All @@ -48,53 +47,10 @@ public class Gala.CloseButton : Clutter.Actor {
var estimated_duration = Utils.get_animation_duration ((uint) (ANIMATION_DURATION * (scale_x - 0.8) / 0.2));
var scale = click_action.pressed ? 0.8 : 1.0;

pixbuf_actor.save_easing_state ();
pixbuf_actor.set_easing_duration (estimated_duration);
pixbuf_actor.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT);
pixbuf_actor.set_scale (scale, scale);
pixbuf_actor.restore_easing_state ();
}

private void load_pixbuf () {
var pixbuf = get_close_button_pixbuf (monitor_scale);
if (pixbuf != null) {
var image = new Gala.Image.from_pixbuf (pixbuf);
pixbuf_actor.set_content (image);
pixbuf_actor.set_size (pixbuf.width, pixbuf.height);
set_size (pixbuf.width, pixbuf.height);
} else {
create_error_texture ();
}
}

private static Gdk.Pixbuf? get_close_button_pixbuf (float monitor_scale) {
var height = Utils.calculate_button_size (monitor_scale);

if (close_pixbufs[height] == null) {
try {
close_pixbufs[height] = new Gdk.Pixbuf.from_resource_at_scale (
"/org/pantheon/desktop/gala/buttons/close.svg",
-1,
height,
true
);
} catch (Error e) {
critical (e.message);
return null;
}
}

return close_pixbufs[height];
}

private void create_error_texture () {
// we'll just make this red so there's at least something as an
// indicator that loading failed. Should never happen and this
// works as good as some weird fallback-image-failed-to-load pixbuf
critical ("Could not create close button");

var size = Utils.calculate_button_size (monitor_scale);
pixbuf_actor.set_size (size, size);
pixbuf_actor.background_color = { 255, 0, 0, 255 };
icon.save_easing_state ();
icon.set_easing_duration (estimated_duration);
icon.set_easing_mode (Clutter.AnimationMode.EASE_IN_OUT);
icon.set_scale (scale, scale);
icon.restore_easing_state ();
}
}
94 changes: 94 additions & 0 deletions lib/Icon.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2025 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <[email protected]>
*/

public class Gala.Icon : Clutter.Actor {
private class ResourceIconSource : Object, IconSource {
public string path { get; construct; }

public ResourceIconSource (string path) {
Object (path: path);
}

public string? get_cache_key (int size, float scale) {
return "%s-%d".printf (path, get_texture_size (size, scale));
}

public Gdk.Pixbuf create_pixbuf (int size, float scale) throws Error {
return new Gdk.Pixbuf.from_resource_at_scale (path, -1, get_texture_size (size, scale), true);
}
}

private interface IconSource : Object {
public abstract string? get_cache_key (int size, float scale);
/**
* Should look up the icon for the given size (e.g. if there are more detailed icons
* for larger sizes) and return a texture of size * scale.
*/
public abstract Gdk.Pixbuf create_pixbuf (int size, float scale) throws Error;

protected static int get_texture_size (int size, float scale) {
return (int) Math.ceilf (size * scale);
}
}

private static HashTable<string, Gdk.Pixbuf> icon_pixbufs = new HashTable<string, Gdk.Pixbuf> (str_hash, str_equal);

public int icon_size { get; construct set; }
public float monitor_scale { get; construct set; }

public string resource_path { set { source = new ResourceIconSource (value); } }

private IconSource _source;
private IconSource source {
get { return _source; }
set {
_source = value;
load_pixbuf ();
}
}

public Icon.from_resource (int icon_size, float monitor_scale, string resource_path) {
Object (icon_size: icon_size, monitor_scale: monitor_scale, resource_path: resource_path);
}

construct {
notify["icon-size"].connect (load_pixbuf);
notify["monitor-scale"].connect (load_pixbuf);
resource_scale_changed.connect (load_pixbuf);
}

private void load_pixbuf () {
var actor_size = Utils.scale_to_int (icon_size, monitor_scale);
set_size (actor_size, actor_size);

var scale = monitor_scale * get_resource_scale ();

try {
var pixbuf = get_pixbuf (icon_size, scale);
content = new Gala.Image.from_pixbuf_with_size (actor_size, actor_size, pixbuf);

set_background_color (null);
} catch (Error e) {
critical ("Could not load icon pixbuf: %s", e.message);
background_color = { 255, 0, 0, 255 };
}
}

private Gdk.Pixbuf get_pixbuf (int size, float scale) throws Error {
var cache_key = source.get_cache_key (size, scale);

if (cache_key == null) {
return source.create_pixbuf (size, scale);
}

if (!(cache_key in icon_pixbufs)) {
icon_pixbufs[cache_key] = source.create_pixbuf (size, scale);
}

return icon_pixbufs[cache_key];
}
}
65 changes: 43 additions & 22 deletions lib/Image.vala
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,23 @@
* Copyright 2025 elementary, Inc. <https://elementary.io>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#if !HAS_MUTTER46
public class Gala.Image : Clutter.Image, Clutter.Content {
private int width;
private int height;

public Image.from_pixbuf_with_size (int width, int height, Gdk.Pixbuf pixbuf) {
Object ();

this.width = width;
this.height = height;

Cogl.PixelFormat pixel_format = (pixbuf.get_has_alpha () ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888);
try {
set_data (pixbuf.get_pixels (), pixel_format, pixbuf.width, pixbuf.height, pixbuf.rowstride);
} catch (Error e) {}
}

#if !HAS_MUTTER48
public class Gala.Image : Clutter.Image {
public Image.from_pixbuf (Gdk.Pixbuf pixbuf) {
Object ();

Expand All @@ -14,19 +28,27 @@ public class Gala.Image : Clutter.Image {
set_data (pixbuf.get_pixels (), pixel_format, pixbuf.width, pixbuf.height, pixbuf.rowstride);
} catch (Error e) {}
}

public override bool get_preferred_size (out float width, out float height) {
width = this.width;
height = this.height;
return true;
}
}
#else
public class Gala.Image : GLib.Object, Clutter.Content {
Gdk.Pixbuf? pixbuf;
Cogl.Texture? texture;
uint width;
uint height;
public int width { get; construct; }
public int height { get; construct; }
public Gdk.Pixbuf pixbuf { get; construct; }

private Cogl.Texture? texture;

public Image.from_pixbuf_with_size (int width, int height, Gdk.Pixbuf pixbuf) {
Object (width: width, height: height, pixbuf: pixbuf);
}

public Image.from_pixbuf (Gdk.Pixbuf pixbuf) {
this.pixbuf = pixbuf;
width = pixbuf.width;
height = pixbuf.height;
invalidate ();
Object (width: pixbuf.width, height: pixbuf.height, pixbuf: pixbuf);
}

public bool get_preferred_size (out float width, out float height) {
Expand All @@ -36,22 +58,21 @@ public class Gala.Image : GLib.Object, Clutter.Content {
return false;
}

width = texture.get_width ();
height = texture.get_height ();
width = this.width;
height = this.height;
return true;
}

public void paint_content (Clutter.Actor actor, Clutter.PaintNode node, Clutter.PaintContext paint_context) {
if (pixbuf != null && texture == null) {
#if HAS_MUTTER48
var cogl_context = actor.context.get_backend ().get_cogl_context ();
#else
var cogl_context = Clutter.get_default_backend ().get_cogl_context ();
#endif
Cogl.PixelFormat pixel_format = (pixbuf.get_has_alpha () ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888);
try {
texture = new Cogl.Texture2D.from_data (cogl_context, pixbuf.width, pixbuf.height, pixel_format, pixbuf.rowstride, pixbuf.get_pixels ());
if (width != texture.get_width () || height != texture.get_height ()) {
width = texture.get_width ();
height = texture.get_height ();
invalidate_size ();
}
} catch (Error e) {
critical (e.message);
}
Expand All @@ -61,15 +82,15 @@ public class Gala.Image : GLib.Object, Clutter.Content {
return;

var content_node = actor.create_texture_paint_node (texture);

#if HAS_MUTTER48
content_node.set_static_name ("Image Content");
#endif
node.add_child (content_node);
}

public void invalidate () {
texture = null;
}
public void invalidate () { }

public void invalidate_size () {
}
public void invalidate_size () { }
}
#endif
2 changes: 1 addition & 1 deletion lib/Utils.vala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

namespace Gala {
public class Utils {
private const int BUTTON_SIZE = 36;
public const int BUTTON_SIZE = 36;

private struct CachedIcon {
public Gdk.Pixbuf icon;
Expand Down
1 change: 1 addition & 0 deletions lib/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ gala_lib_sources = files(
'Drawing/Color.vala',
'Drawing/StyleManager.vala',
'Drawing/Utilities.vala',
'Icon.vala',
'Image.vala',
'Plugin.vala',
'RoundedCornersEffect.vala',
Expand Down