game-thumbnail.vala 8.71 KB
Newer Older
1
// This file is part of GNOME Games. License: GPL-3.0+.
Adrien Plazas's avatar
Adrien Plazas committed
2

3
private class Games.GameThumbnail : Gtk.DrawingArea {
Adrien Plazas's avatar
Adrien Plazas committed
4 5 6
	private const Gtk.CornerType[] right_corners = { Gtk.CornerType.TOP_RIGHT, Gtk.CornerType.BOTTOM_RIGHT };
	private const Gtk.CornerType[] bottom_corners = { Gtk.CornerType.BOTTOM_LEFT, Gtk.CornerType.BOTTOM_RIGHT };

7
	private const double EMBLEM_SCALE = 0.125;
Adrien Plazas's avatar
Adrien Plazas committed
8 9
	private const double ICON_SCALE = 0.75;

10 11 12 13 14 15 16 17 18 19 20 21 22
	private Uid _uid;
	public Uid uid {
		get { return _uid; }
		set {
			if (_uid == value)
				return;

			_uid = value;

			queue_draw ();
		}
	}

23 24 25
	private Icon _icon;
	public Icon icon {
		get { return _icon; }
Adrien Plazas's avatar
Adrien Plazas committed
26
		set {
27
			if (_icon == value)
Adrien Plazas's avatar
Adrien Plazas committed
28 29
				return;

30
			_icon = value;
Adrien Plazas's avatar
Adrien Plazas committed
31 32 33 34 35

			queue_draw ();
		}
	}

36 37 38 39 40 41 42 43 44 45 46 47 48 49
	private ulong cover_changed_id;
	private Cover _cover;
	public Cover cover {
		get { return _cover; }
		set {
			if (_cover == value)
				return;

			if (_cover != null)
				_cover.disconnect (cover_changed_id);

			_cover = value;

			if (_cover != null)
50
				cover_changed_id = _cover.changed.connect (invalidate_cover);
51

52
			invalidate_cover ();
53 54 55
		}
	}

56
	private bool tried_loading_cover;
57 58 59 60
	private Gdk.Pixbuf? cover_cache;
	private int previous_cover_width;
	private int previous_cover_height;

Adrien Plazas's avatar
Adrien Plazas committed
61 62 63 64 65 66 67 68 69
	public struct DrawingContext {
		Cairo.Context cr;
		Gdk.Window? window;
		Gtk.StyleContext style;
		Gtk.StateFlags state;
		int width;
		int height;
	}

Adrien Plazas's avatar
Adrien Plazas committed
70 71 72 73
	static construct {
		set_css_name ("gamesgamethumbnail");
	}

74 75 76 77 78 79 80 81
	public override Gtk.SizeRequestMode get_request_mode () {
		return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
	}

	public override void get_preferred_height_for_width (int width, out int minimum_height, out int natural_height) {
		minimum_height = natural_height = width;
	}

Adrien Plazas's avatar
Adrien Plazas committed
82 83 84 85 86 87 88 89 90 91 92
	public override bool draw (Cairo.Context cr) {
		var window = get_window ();
		var style = get_style_context ();
		var state = get_state_flags ();
		var width = get_allocated_width ();
		var height = get_allocated_height ();

		DrawingContext context = {
			cr, window, style, state, width, height
		};

93
		if (icon == null)
Adrien Plazas's avatar
Adrien Plazas committed
94 95
			return false;

96 97 98
		if (cover == null)
			return false;

Adrien Plazas's avatar
Adrien Plazas committed
99 100
		var drawn = false;

101 102 103 104
		drawn = draw_cover (context);

		if (!drawn)
			drawn = draw_icon (context);
Adrien Plazas's avatar
Adrien Plazas committed
105

Adrien Plazas's avatar
Adrien Plazas committed
106 107 108 109 110 111 112
		// Draw the default thumbnail if no thumbnail have been drawn
		if (!drawn)
			draw_default (context);

		return true;
	}

Adrien Plazas's avatar
Adrien Plazas committed
113
	public bool draw_icon (DrawingContext context) {
114 115
		var g_icon = icon.get_icon ();
		if (g_icon == null)
Adrien Plazas's avatar
Adrien Plazas committed
116 117
			return false;

118
		var pixbuf = get_scaled_icon (context, g_icon, ICON_SCALE);
Adrien Plazas's avatar
Adrien Plazas committed
119 120 121 122 123 124 125 126 127 128
		if (pixbuf == null)
			return false;

		draw_background (context);
		draw_pixbuf (context, pixbuf);
		draw_border (context);

		return true;
	}

129
	public bool draw_cover (DrawingContext context) {
130
		var pixbuf = get_scaled_cover (context);
131 132 133
		if (pixbuf == null)
			return false;

134 135
		var border_radius = (int) context.style.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS, context.state);

Adrien Plazas's avatar
Adrien Plazas committed
136
		context.cr.set_source_rgb (0, 0, 0);
137
		rounded_rectangle (context.cr, 0.5, 0.5, context.width - 1, context.height - 1, border_radius);
Adrien Plazas's avatar
Adrien Plazas committed
138
		context.cr.fill ();
139
		draw_pixbuf (context, pixbuf);
Adrien Plazas's avatar
Adrien Plazas committed
140
		draw_border (context);
141 142 143 144

		return true;
	}

Adrien Plazas's avatar
Adrien Plazas committed
145 146
	public void draw_default (DrawingContext context) {
		draw_background (context);
147
		draw_emblem_icon (context, "applications-games-symbolic", EMBLEM_SCALE);
Adrien Plazas's avatar
Adrien Plazas committed
148 149 150
		draw_border (context);
	}

151
	private void draw_emblem_icon (DrawingContext context, string icon_name, double scale) {
Adrien Plazas's avatar
Adrien Plazas committed
152 153 154 155 156
		Gdk.Pixbuf? emblem = null;

		var color = context.style.get_color (context.state);

		var theme = Gtk.IconTheme.get_default ();
157
		var size = int.min (context.width, context.height) * scale;
Adrien Plazas's avatar
Adrien Plazas committed
158
		try {
159
			var icon_info = theme.lookup_icon (icon_name, (int) size, Gtk.IconLookupFlags.FORCE_SIZE);
Adrien Plazas's avatar
Adrien Plazas committed
160 161
			emblem = icon_info.load_symbolic (color);
		} catch (GLib.Error error) {
Adrien Plazas's avatar
Adrien Plazas committed
162
			warning (@"Unable to get icon “$icon_name”: $(error.message)");
Adrien Plazas's avatar
Adrien Plazas committed
163 164 165 166 167 168 169 170 171 172 173 174 175
			return;
		}

		if (emblem == null)
			return;

		double offset_x = context.width / 2.0 - emblem.width / 2.0;
		double offset_y = context.height / 2.0 - emblem.height / 2.0;

		Gdk.cairo_set_source_pixbuf (context.cr, emblem, offset_x, offset_y);
		context.cr.paint ();
	}

Adrien Plazas's avatar
Adrien Plazas committed
176
	private Gdk.Pixbuf? get_scaled_icon (DrawingContext context, GLib.Icon? icon, double scale) {
Adrien Plazas's avatar
Adrien Plazas committed
177 178 179 180 181 182
		if (icon == null)
			return null;

		var theme = Gtk.IconTheme.get_default ();
		var lookup_flags = Gtk.IconLookupFlags.FORCE_SIZE | Gtk.IconLookupFlags.FORCE_REGULAR;
		var size = int.min (context.width, context.height) * scale;
183
		var icon_info = theme.lookup_by_gicon (icon, (int) size, lookup_flags);
Adrien Plazas's avatar
Adrien Plazas committed
184

185 186 187
		if (icon_info == null)
			return null;

Adrien Plazas's avatar
Adrien Plazas committed
188 189 190 191
		try {
			return icon_info.load_icon ();
		}
		catch (Error e) {
Adrien Plazas's avatar
Adrien Plazas committed
192
			warning (@"Couldn’t load the icon: $(e.message)\n");
Adrien Plazas's avatar
Adrien Plazas committed
193 194 195 196
			return null;
		}
	}

197 198 199 200
	private Gdk.Pixbuf? get_scaled_cover (DrawingContext context) {
		if (previous_cover_width != context.width) {
			previous_cover_width = context.width;
			cover_cache = null;
201
			tried_loading_cover = false;
202 203 204 205 206
		}

		if (previous_cover_height != context.height) {
			previous_cover_height = context.height;
			cover_cache = null;
207
			tried_loading_cover = false;
208 209 210 211 212
		}

		if (cover_cache != null)
			return cover_cache;

213 214
		var size = int.min (context.width, context.height);

215 216
		load_cover_cache_from_disk (context, size);
		if (cover_cache != null)
217 218
			return cover_cache;

219 220
		var g_icon = cover.get_cover ();
		if (g_icon == null)
221 222 223 224
			return null;

		var theme = Gtk.IconTheme.get_default ();
		var lookup_flags = Gtk.IconLookupFlags.FORCE_SIZE | Gtk.IconLookupFlags.FORCE_REGULAR;
225
		var icon_info = theme.lookup_by_gicon (g_icon, (int) size, lookup_flags);
226 227

		try {
228
			cover_cache = icon_info.load_icon ();
229
			save_cover_cache_to_disk (size);
230 231
		}
		catch (Error e) {
Adrien Plazas's avatar
Adrien Plazas committed
232
			warning (@"Couldn’t load the icon: $(e.message)\n");
233
		}
234

235 236 237 238
		return cover_cache;
	}

	private void load_cover_cache_from_disk (DrawingContext context, int size) {
239 240 241 242 243
		if (tried_loading_cover)
			return;

		tried_loading_cover = true;

244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
		string cover_cache_path;
		try {
			cover_cache_path = get_cover_cache_path (size);
		}
		catch (Error e) {
			critical (e.message);

			return;
		}

		try {
			cover_cache = new Gdk.Pixbuf.from_file_at_scale (cover_cache_path, context.width,
			                                                 context.height, true);
		}
		catch (Error e) {
			debug (e.message);
		}
	}

	private void save_cover_cache_to_disk (int size) {
		if (cover_cache == null)
			return;

267 268 269 270 271
		Application.try_make_dir (Application.get_covers_cache_dir (size));
		var now = new GLib.DateTime.now_local ();
		var creation_time = now.to_string ();

		try {
272
			var cover_cache_path = get_cover_cache_path (size);
273 274 275 276 277 278 279 280
			cover_cache.save (cover_cache_path, "png",
			                  "tEXt::Software", "GNOME Games",
			                  "tEXt::Creation Time", creation_time.to_string (),
			                  null);
		}
		catch (Error e) {
			critical (e.message);
		}
281 282
	}

283 284 285 286 287 288 289 290 291 292
	private string get_cover_cache_path (int size) throws Error {
		var dir = Application.get_covers_cache_dir (size);

		assert (uid != null);

		var uid = uid.get_uid ();

		return @"$dir/$uid.png";
	}

293
	private void invalidate_cover () {
294
		cover_cache = null;
295
		tried_loading_cover = false;
296 297 298
		queue_draw ();
	}

Adrien Plazas's avatar
Adrien Plazas committed
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
	private void draw_pixbuf (DrawingContext context, Gdk.Pixbuf pixbuf) {
		var surface = Gdk.cairo_surface_create_from_pixbuf (pixbuf, 1, context.window);

		var mask = get_mask (context);

		var x_offset = (context.width - pixbuf.width) / 2;
		var y_offset = (context.height - pixbuf.height) / 2;

		context.cr.set_source_surface (surface, x_offset, y_offset);
		context.cr.mask_surface (mask, 0, 0);
	}

	private Cairo.Surface get_mask (DrawingContext context) {
		Cairo.ImageSurface mask = new Cairo.ImageSurface (Cairo.Format.A8, context.width, context.height);

314 315
		var border_radius = (int) context.style.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS, context.state);

Adrien Plazas's avatar
Adrien Plazas committed
316
		Cairo.Context cr = new Cairo.Context (mask);
Adrien Plazas's avatar
Adrien Plazas committed
317
		cr.set_source_rgba (0, 0, 0, 0.9);
318
		rounded_rectangle (cr, 0.5, 0.5, context.width - 1, context.height - 1, border_radius);
Adrien Plazas's avatar
Adrien Plazas committed
319 320 321 322 323
		cr.fill ();

		return mask;
	}

Adrien Plazas's avatar
Adrien Plazas committed
324
	private void draw_background (DrawingContext context) {
Adrien Plazas's avatar
Adrien Plazas committed
325
		context.style.render_background (context.cr, 0.0, 0.0, context.width, context.height);
Adrien Plazas's avatar
Adrien Plazas committed
326 327 328
	}

	private void draw_border (DrawingContext context) {
Adrien Plazas's avatar
Adrien Plazas committed
329
		context.style.render_frame (context.cr, 0.0, 0.0, context.width, context.height);
Adrien Plazas's avatar
Adrien Plazas committed
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
	}

	private void rounded_rectangle (Cairo.Context cr, double x, double y, double width, double height, double radius) {
		const double ARC_0 = 0;
		const double ARC_1 = Math.PI * 0.5;
		const double ARC_2 = Math.PI;
		const double ARC_3 = Math.PI * 1.5;

		cr.new_sub_path ();
		cr.arc (x + width - radius, y + radius,	         radius, ARC_3, ARC_0);
		cr.arc (x + width - radius, y + height - radius, radius, ARC_0, ARC_1);
		cr.arc (x + radius,         y + height - radius, radius, ARC_1, ARC_2);
		cr.arc (x + radius,         y + radius,          radius, ARC_2, ARC_3);
		cr.close_path ();
	}
}