pip: Add Picture In Picture plugin (#32)

This commit is contained in:
Adam Bieńkowski 2017-07-08 08:30:31 +02:00 committed by Rico Tzschichholz
parent 3b064fa99e
commit 52678172d3
9 changed files with 1016 additions and 0 deletions

View File

@ -301,6 +301,7 @@ vapi/Makefile
plugins/Makefile
plugins/maskcorners/Makefile
plugins/notify/Makefile
plugins/pip/Makefile
plugins/zoom/Makefile
po/Makefile.in
])

View File

@ -141,6 +141,11 @@
<summary>Cycle to previous keyboard layout</summary>
<description>DEPRECATED: This key is deprecated and ignored.</description>
</key>
<key type="as" name="pip">
<default><![CDATA[['<Super>D']]]></default>
<summary>The shortcut to enable picture-in-picture window</summary>
<description>The shortcut to show the selection area to choose a window</description>
</key>
</schema>
<schema path="/org/pantheon/desktop/gala/appearance/" id="org.pantheon.desktop.gala.appearance" gettext-domain="@GETTEXT_PACKAGE@">

View File

@ -1,6 +1,7 @@
SUBDIRS = \
maskcorners \
notify \
pip \
zoom \
$(NULL)

191
plugins/pip/Main.vala Normal file
View File

@ -0,0 +1,191 @@
//
// Copyright (C) 2017 Adam Bieńkowski
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
public class Gala.Plugins.PIP.Plugin : Gala.Plugin
{
private const int MIN_SELECTION_SIZE = 30;
private Gee.ArrayList<PopupWindow> windows;
private Gala.WindowManager? wm = null;
private SelectionArea? selection_area;
construct
{
windows = new Gee.ArrayList<PopupWindow> ();
}
public override void initialize (Gala.WindowManager wm)
{
this.wm = wm;
var display = wm.get_screen ().get_display ();
var settings = new GLib.Settings (Config.SCHEMA + ".keybindings");
display.add_keybinding ("pip", settings, Meta.KeyBindingFlags.NONE, (Meta.KeyHandlerFunc) on_initiate);
}
public override void destroy ()
{
clear_selection_area ();
foreach (var popup_window in windows) {
untrack_window (popup_window);
}
windows.clear ();
}
[CCode (instance_pos = -1)]
void on_initiate (Meta.Display display, Meta.Screen screen,
Meta.Window? window, Clutter.KeyEvent event, Meta.KeyBinding binding)
{
selection_area = new SelectionArea (wm);
selection_area.selected.connect (on_selection_actor_selected);
selection_area.captured.connect (on_selection_actor_captured);
selection_area.closed.connect (clear_selection_area);
track_actor (selection_area);
wm.ui_group.add_child (selection_area);
selection_area.start_selection ();
}
private void on_selection_actor_selected (int x, int y)
{
clear_selection_area ();
select_window_at (x, y);
}
private void on_selection_actor_captured (int x, int y, int width, int height)
{
clear_selection_area ();
if (width < MIN_SELECTION_SIZE || height < MIN_SELECTION_SIZE) {
select_window_at (x, y);
} else {
var active = get_active_window_actor ();
if (active != null) {
int point_x = x - (int)active.x;
int point_y = y - (int)active.y;
var rect = Clutter.Rect.alloc ();
var clip = rect.init (point_x, point_y, width, height);
var popup_window = new PopupWindow (wm, active, clip);
add_window (popup_window);
}
}
}
private void select_window_at (int x, int y)
{
var selected = get_window_actor_at (x, y);
if (selected != null) {
var popup_window = new PopupWindow (wm, selected, null);
add_window (popup_window);
}
}
private void clear_selection_area ()
{
if (selection_area != null) {
untrack_actor (selection_area);
update_region ();
selection_area.destroy ();
selection_area = null;
}
}
private Meta.WindowActor? get_window_actor_at (float x, float y)
{
var screen = wm.get_screen ();
unowned List<weak Meta.WindowActor> actors = Meta.Compositor.get_window_actors (screen);
var copy = actors.copy ();
copy.reverse ();
weak Meta.WindowActor? selected = null;
copy.@foreach ((actor) => {
if (selected != null) {
return;
}
var window = actor.get_meta_window ();
var bbox = actor.get_allocation_box ();
if (!window.is_hidden () && !window.is_skip_taskbar () && bbox.contains (x, y)) {
selected = actor;
}
});
return selected;
}
private Meta.WindowActor? get_active_window_actor ()
{
var screen = wm.get_screen ();
unowned List<weak Meta.WindowActor> actors = Meta.Compositor.get_window_actors (screen);
var copy = actors.copy ();
copy.reverse ();
weak Meta.WindowActor? active = null;
actors.@foreach ((actor) => {
if (active != null) {
return;
}
var window = actor.get_meta_window ();
if (!window.is_hidden () && !window.is_skip_taskbar () && window.has_focus ()) {
active = actor;
}
});
return active;
}
private void add_window (PopupWindow popup_window)
{
popup_window.closed.connect (() => remove_window (popup_window));
windows.add (popup_window);
track_actor (popup_window);
wm.ui_group.add_child (popup_window);
}
private void remove_window (PopupWindow popup_window)
{
windows.remove (popup_window);
untrack_window (popup_window);
}
private void untrack_window (PopupWindow popup_window)
{
untrack_actor (popup_window);
update_region ();
popup_window.destroy ();
}
}
public Gala.PluginInfo register_plugin ()
{
return Gala.PluginInfo () {
name = "Popup Window",
author = "Adam Bieńkowski <donadigos159@gmail.com>",
plugin_type = typeof (Gala.Plugins.PIP.Plugin),
provides = Gala.PluginFunction.ADDITION,
load_priority = Gala.LoadPriority.IMMEDIATE
};
}

61
plugins/pip/Makefile.am Normal file
View File

@ -0,0 +1,61 @@
include $(top_srcdir)/Makefile.common
VAPIDIR = $(top_srcdir)/vapi
BUILT_SOURCES = libgala_pip_la_vala.stamp
libgala_pip_la_LTLIBRARIES = libgala-pip.la
libgala_pip_ladir = $(pkglibdir)/plugins
libgala_pip_la_LDFLAGS = \
$(PLUGIN_LDFLAGS) \
$(GALA_CORE_LDFLAGS) \
$(top_builddir)/lib/libgala.la \
$(NULL)
libgala_pip_la_CFLAGS = \
$(GALA_CORE_CFLAGS) \
-include config.h \
-w \
-I$(top_builddir)/lib \
$(NULL)
libgala_pip_la_VALAFLAGS = \
$(GALA_CORE_VALAFLAGS) \
$(top_builddir)/lib/gala.vapi \
--vapidir $(VAPIDIR) \
$(VAPIDIR)/config.vapi \
$(NULL)
libgala_pip_la_LIBADD = \
$(GALA_CORE_LIBS) \
$(NULL)
libgala_pip_la_VALASOURCES = \
Main.vala \
MoveAction.vala \
PopupWindow.vala \
SelectionArea.vala \
ShadowEffect.vala \
$(NULL)
nodist_libgala_pip_la_SOURCES = \
$(BUILT_SOURCES) \
$(libgala_pip_la_VALASOURCES:.vala=.c) \
$(NULL)
libgala_pip_la_vala.stamp: $(libgala_pip_la_VALASOURCES) $(top_builddir)/lib/gala.vapi Makefile
$(AM_V_VALA)$(VALAC) \
$(libgala_pip_la_VALAFLAGS) \
-C \
$(filter %.vala %.c,$^)
$(AM_V_at)touch $@
CLEANFILES = \
$(nodist_libgala_pip_la_SOURCES) \
$(NULL)
EXTRA_DIST = \
$(libgala_pip_la_VALASOURCES) \
$(NULL)

View File

@ -0,0 +1,27 @@
//
// Copyright (C) 2017 Adam Bieńkowski
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
public class Gala.Plugins.PIP.MoveAction : Clutter.DragAction
{
public signal void move ();
public override bool drag_progress (Clutter.Actor actor, float delta_x, float delta_y)
{
move ();
return false;
}
}

View File

@ -0,0 +1,403 @@
//
// Copyright (C) 2017 Adam Bieńkowski
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
public class Gala.Plugins.PIP.PopupWindow : Clutter.Actor
{
private const int BUTTON_SIZE = 36;
private const int CONTAINER_MARGIN = BUTTON_SIZE / 2;
private const int SHADOW_SIZE = 100;
private const uint FADE_OUT_TIMEOUT = 200;
private const float MINIMUM_SCALE = 0.1f;
private const float MAXIMUM_SCALE = 1.0f;
private const int SCREEN_MARGIN = 0;
private static Clutter.Image? resize_image;
public signal void closed ();
public Gala.WindowManager wm { get; construct; }
public Meta.WindowActor window_actor { get; construct; }
public Clutter.Rect? container_clip { get; construct; }
private Clutter.Actor clone;
private Clutter.Actor container;
private Clutter.Actor close_button;
private Clutter.Actor resize_button;
private Clutter.Actor resize_handle;
private Clutter.ClickAction close_action;
private Clutter.DragAction resize_action;
private MoveAction move_action;
private bool dragging = false;
private bool clicked = false;
private int x_offset_press = 0;
private int y_offset_press = 0;
private float begin_resize_width = 0.0f;
private float begin_resize_height = 0.0f;
// From https://opensourcehacker.com/2011/12/01/calculate-aspect-ratio-conserving-resize-for-images-in-javascript/
static void calculate_aspect_ratio_size_fit (float src_width, float src_height, float max_width, float max_height,
out float width, out float height)
{
float ratio = float.min (max_width / src_width, max_height / src_height);
width = src_width * ratio;
height = src_height * ratio;
}
static Clutter.Image? get_resize_image ()
{
if (resize_image == null) {
try {
string filename = Path.build_filename (Config.PKGDATADIR, "resize.svg");
var pixbuf = new Gdk.Pixbuf.from_file (filename);
resize_image = new Clutter.Image ();
resize_image.set_data (pixbuf.get_pixels (),
Cogl.PixelFormat.RGBA_8888,
pixbuf.get_width (),
pixbuf.get_height (),
pixbuf.get_rowstride ());
} catch (Error e) {
warning (e.message);
}
}
return resize_image;
}
static void get_current_cursor_position (out int x, out int y)
{
Gdk.Display.get_default ().get_device_manager ().get_client_pointer ().get_position (null, out x, out y);
}
public PopupWindow (Gala.WindowManager wm, Meta.WindowActor window_actor, Clutter.Rect? container_clip)
{
Object (wm: wm, window_actor: window_actor, container_clip: container_clip);
}
construct
{
reactive = true;
set_pivot_point (0.5f, 0.5f);
set_easing_mode (Clutter.AnimationMode.EASE_IN_QUAD);
var window = window_actor.get_meta_window ();
window.unmanaged.connect (on_close_click_clicked);
clone = new Clutter.Clone (window_actor.get_texture ());
move_action = new MoveAction ();
move_action.drag_begin.connect (on_move_begin);
move_action.drag_end.connect (on_move_end);
move_action.move.connect (on_move);
container = new Clutter.Actor ();
container.reactive = true;
container.set_scale (0.35f, 0.35f);
container.clip_rect = container_clip;
container.add_effect (new Gala.ShadowEffect (SHADOW_SIZE, 2));
container.add_child (clone);
container.add_action (move_action);
if (container_clip == null) {
window_actor.notify["allocation"].connect (on_allocation_changed);
container.set_position (CONTAINER_MARGIN, CONTAINER_MARGIN);
}
update_size ();
update_container_position ();
Meta.Rectangle monitor_rect;
get_current_monitor_rect (out monitor_rect);
set_position (SCREEN_MARGIN + monitor_rect.x, monitor_rect.height + monitor_rect.y - SCREEN_MARGIN - height);
close_action = new Clutter.ClickAction ();
close_action.clicked.connect (on_close_click_clicked);
close_button = Gala.Utils.create_close_button ();
close_button.set_size (BUTTON_SIZE, BUTTON_SIZE);
close_button.opacity = 0;
close_button.reactive = true;
close_button.set_easing_duration (300);
close_button.add_action (close_action);
resize_action = new Clutter.DragAction ();
resize_action.drag_begin.connect (on_resize_drag_begin);
resize_action.drag_end.connect (on_resize_drag_end);
resize_action.drag_motion.connect (on_resize_drag_motion);
resize_handle = new Clutter.Actor ();
resize_handle.set_size (BUTTON_SIZE, BUTTON_SIZE);
resize_handle.set_pivot_point (0.5f, 0.5f);
resize_handle.set_position (width - BUTTON_SIZE, height - BUTTON_SIZE);
resize_handle.reactive = true;
resize_handle.add_action (resize_action);
resize_button = new Clutter.Actor ();
resize_button.set_pivot_point (0.5f, 0.5f);
resize_button.set_size (BUTTON_SIZE, BUTTON_SIZE);
resize_button.set_position (width - BUTTON_SIZE, height - BUTTON_SIZE);
resize_button.opacity = 0;
resize_button.reactive = true;
resize_button.content = get_resize_image ();
add_child (container);
add_child (close_button);
add_child (resize_button);
add_child (resize_handle);
}
public override void show ()
{
base.show ();
opacity = 0;
set_easing_duration (200);
opacity = 255;
set_easing_duration (0);
}
public override bool enter_event (Clutter.CrossingEvent event)
{
close_button.opacity = 255;
resize_button.set_easing_duration (300);
resize_button.opacity = 255;
resize_button.set_easing_duration (0);
return true;
}
public override bool leave_event (Clutter.CrossingEvent event)
{
close_button.opacity = 0;
resize_button.set_easing_duration (300);
resize_button.opacity = 0;
resize_button.set_easing_duration (0);
return true;
}
private void on_move_begin ()
{
int px, py;
get_current_cursor_position (out px, out py);
x_offset_press = (int)(px - x);
y_offset_press = (int)(py - y);
clicked = true;
dragging = false;
}
private void on_move_end ()
{
clicked = false;
if (dragging) {
update_screen_position ();
dragging = false;
} else {
activate ();
}
}
private void on_move ()
{
if (!clicked) {
return;
}
float motion_x, motion_y;
move_action.get_motion_coords (out motion_x, out motion_y);
x = (int)motion_x - x_offset_press;
y = (int)motion_y - y_offset_press;
if (!dragging) {
dragging = true;
}
}
private void on_resize_drag_begin (Clutter.Actor actor, float event_x, float event_y, Clutter.ModifierType type)
{
begin_resize_width = width;
begin_resize_height = height;
}
private void on_resize_drag_end (Clutter.Actor actor, float event_x, float event_y, Clutter.ModifierType type)
{
reposition_resize_handle ();
update_screen_position ();
}
private void on_resize_drag_motion (Clutter.Actor actor, float delta_x, float delta_y)
{
float press_x, press_y;
resize_action.get_press_coords (out press_x, out press_y);
int motion_x, motion_y;
get_current_cursor_position (out motion_x, out motion_y);
float diff_x = motion_x - press_x;
float diff_y = motion_y - press_y;
width = begin_resize_width + diff_x;
height = begin_resize_height + diff_y;
update_container_scale ();
update_size ();
reposition_resize_button ();
}
private void on_allocation_changed ()
{
update_size ();
reposition_resize_button ();
reposition_resize_handle ();
}
private void on_close_click_clicked ()
{
set_easing_duration (FADE_OUT_TIMEOUT);
opacity = 0;
Clutter.Threads.Timeout.add (FADE_OUT_TIMEOUT, () => {
closed ();
return false;
});
}
private void update_size ()
{
if (container_clip != null) {
width = (int)(container_clip.get_width () * container.scale_x + BUTTON_SIZE);
height = (int)(container_clip.get_height () * container.scale_y + BUTTON_SIZE);
} else {
width = (int)(container.width * container.scale_x + BUTTON_SIZE);
height = (int)(container.height * container.scale_y + BUTTON_SIZE);
}
}
private void update_container_scale ()
{
float src_width;
float src_height;
if (container_clip != null) {
src_width = container_clip.get_width ();
src_height = container_clip.get_height ();
} else {
src_width = container.width;
src_height = container.height;
}
float max_width = width - BUTTON_SIZE;
float max_height = height - BUTTON_SIZE;
float new_width, new_height;
calculate_aspect_ratio_size_fit (
src_width, src_height,
max_width, max_height,
out new_width, out new_height
);
float window_width, window_height;
get_target_window_size (out window_width, out window_height);
float new_scale_x = new_width / window_width;
float new_scale_y = new_height / window_height;
container.scale_x = new_scale_x.clamp (MINIMUM_SCALE, MAXIMUM_SCALE);
container.scale_y = new_scale_y.clamp (MINIMUM_SCALE, MAXIMUM_SCALE);
update_container_position ();
}
private void update_container_position ()
{
if (container_clip != null) {
container.x = (float)(-container_clip.get_x () * container.scale_x + CONTAINER_MARGIN);
container.y = (float)(-container_clip.get_y () * container.scale_y + CONTAINER_MARGIN);
}
}
private void update_screen_position ()
{
Meta.Rectangle monitor_rect;
get_current_monitor_rect (out monitor_rect);
int monitor_x = monitor_rect.x;
int monitor_y = monitor_rect.y;
int monitor_width = monitor_rect.width;
int monitor_height = monitor_rect.height;
set_easing_duration (300);
set_easing_mode (Clutter.AnimationMode.EASE_OUT_BACK);
var screen_limit_start = SCREEN_MARGIN + monitor_x;
var screen_limit_end = monitor_width + monitor_x - SCREEN_MARGIN - width;
x = x.clamp (screen_limit_start, screen_limit_end);
screen_limit_start = SCREEN_MARGIN + monitor_y;
screen_limit_end = monitor_height + monitor_y - SCREEN_MARGIN - height;
y = y.clamp (screen_limit_start, screen_limit_end);
set_easing_mode (Clutter.AnimationMode.EASE_IN_QUAD);
set_easing_duration (0);
}
private void reposition_resize_button ()
{
resize_button.set_position (width - BUTTON_SIZE, height - BUTTON_SIZE);
}
private void reposition_resize_handle ()
{
resize_handle.set_position (width - BUTTON_SIZE, height - BUTTON_SIZE);
}
private void get_current_monitor_rect (out Meta.Rectangle rect)
{
var screen = wm.get_screen ();
rect = screen.get_monitor_geometry (screen.get_current_monitor ());
}
private void get_target_window_size (out float width, out float height)
{
if (container_clip != null) {
width = container_clip.get_width ();
height = container_clip.get_height ();
} else {
width = window_actor.width;
height = window_actor.height;
}
}
private void activate ()
{
var window = window_actor.get_meta_window ();
window.activate (Clutter.get_current_event_time ());
}
}

View File

@ -0,0 +1,174 @@
//
// Copyright (C) 2017 Santiago León O., Adam Bieńkowski
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
public class Gala.Plugins.PIP.SelectionArea : Clutter.Actor
{
public signal void captured (int x, int y, int width, int height);
public signal void selected (int x, int y);
public signal void closed ();
public Gala.WindowManager wm { get; construct; }
private Gala.ModalProxy? modal_proxy;
private Gdk.Point start_point;
private Gdk.Point end_point;
private bool dragging = false;
private bool clicked = false;
public SelectionArea (Gala.WindowManager wm)
{
Object (wm: wm);
}
construct
{
start_point = { 0, 0 };
end_point = { 0, 0 };
visible = true;
reactive = true;
int screen_width, screen_height;
wm.get_screen ().get_size (out screen_width, out screen_height);
width = screen_width;
height = screen_height;
var canvas = new Clutter.Canvas ();
canvas.set_size (screen_width, screen_height);
canvas.draw.connect (draw_area);
set_content (canvas);
canvas.invalidate ();
}
public override bool key_press_event (Clutter.KeyEvent e)
{
if (e.keyval == Clutter.Key.Escape) {
close ();
closed ();
return true;
}
return false;
}
public override bool button_press_event (Clutter.ButtonEvent e)
{
if (dragging || e.button != 1) {
return true;
}
clicked = true;
start_point.x = (int) e.x;
start_point.y = (int) e.y;
return true;
}
public override bool button_release_event (Clutter.ButtonEvent e)
{
if (e.button != 1) {
return true;
}
if (!dragging) {
selected ((int) e.x, (int) e.y);
close ();
return true;
}
dragging = false;
clicked = false;
int x, y, w, h;
get_selection_rectangle (out x, out y, out w, out h);
close ();
start_point = { 0, 0 };
end_point = { 0, 0 };
this.hide ();
content.invalidate ();
captured (x, y, w, h);
return true;
}
public override bool motion_event (Clutter.MotionEvent e)
{
if (!clicked) {
return true;
}
end_point.x = (int) e.x;
end_point.y = (int) e.y;
content.invalidate ();
if (!dragging) {
dragging = true;
}
return true;
}
public void close ()
{
wm.get_screen ().set_cursor (Meta.Cursor.DEFAULT);
if (modal_proxy != null) {
wm.pop_modal (modal_proxy);
}
}
public void start_selection ()
{
wm.get_screen ().set_cursor (Meta.Cursor.CROSSHAIR);
grab_key_focus ();
modal_proxy = wm.push_modal ();
}
private void get_selection_rectangle (out int x, out int y, out int width, out int height)
{
x = int.min (start_point.x, end_point.x);
y = int.min (start_point.y, end_point.y);
width = (start_point.x - end_point.x).abs ();
height = (start_point.y - end_point.y).abs ();
}
private bool draw_area (Cairo.Context ctx)
{
Clutter.cairo_clear (ctx);
if (!dragging) {
return true;
}
int x, y, w, h;
get_selection_rectangle (out x, out y, out w, out h);
ctx.rectangle (x, y, w, h);
ctx.set_source_rgba (0.1, 0.1, 0.1, 0.2);
ctx.fill ();
ctx.rectangle (x, y, w, h);
ctx.set_source_rgb (0.7, 0.7, 0.7);
ctx.set_line_width (1.0);
ctx.stroke ();
return true;
}
}

View File

@ -0,0 +1,153 @@
//
// Copyright (C) 2014 Tom Beckmann
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
using Clutter;
namespace Gala
{
public class ShadowEffect : Effect
{
private class Shadow
{
public int users;
public Cogl.Texture texture;
public Shadow (Cogl.Texture _texture)
{
texture = _texture;
users = 1;
}
}
// the sizes of the textures often repeat, especially for the background actor
// so we keep a cache to avoid creating the same texture all over again.
static Gee.HashMap<string,Shadow> shadow_cache;
static construct
{
shadow_cache = new Gee.HashMap<string,Shadow> ();
}
public int shadow_size { get; construct; }
public int shadow_spread { get; construct; }
public float scale_factor { get; set; default = 1; }
public uint8 shadow_opacity { get; set; default = 255; }
Cogl.Material material;
string? current_key = null;
public ShadowEffect (int shadow_size, int shadow_spread)
{
Object (shadow_size: shadow_size, shadow_spread: shadow_spread);
}
construct
{
material = new Cogl.Material ();
}
~ShadowEffect ()
{
if (current_key != null)
decrement_shadow_users (current_key);
}
Cogl.Texture? get_shadow (int width, int height, int shadow_size, int shadow_spread)
{
var old_key = current_key;
current_key = "%ix%i:%i:%i".printf (width, height, shadow_size, shadow_spread);
if (old_key == current_key)
return null;
if (old_key != null)
decrement_shadow_users (old_key);
Shadow? shadow = null;
if ((shadow = shadow_cache.@get (current_key)) != null) {
shadow.users++;
return shadow.texture;
}
// fill a new texture for this size
var buffer = new Granite.Drawing.BufferSurface (width, height);
buffer.context.rectangle (shadow_size - shadow_spread, shadow_size - shadow_spread,
width - shadow_size * 2 + shadow_spread * 2, height - shadow_size * 2 + shadow_spread * 2);
buffer.context.set_source_rgba (0, 0, 0, 0.7);
buffer.context.fill ();
buffer.exponential_blur (shadow_size / 2);
var surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, width, height);
var cr = new Cairo.Context (surface);
cr.set_source_surface (buffer.surface, 0, 0);
cr.paint ();
var texture = new Cogl.Texture.from_data (width, height, 0, Cogl.PixelFormat.BGRA_8888_PRE,
Cogl.PixelFormat.ANY, surface.get_stride (), surface.get_data ());
shadow_cache.@set (current_key, new Shadow (texture));
return texture;
}
void decrement_shadow_users (string key)
{
var shadow = shadow_cache.@get (key);
if (shadow == null)
return;
if (--shadow.users == 0)
shadow_cache.unset (key);
}
public override void paint (EffectPaintFlags flags)
{
var bounding_box = get_bounding_box ();
var shadow = get_shadow ((int) (bounding_box.x2 - bounding_box.x1), (int) (bounding_box.y2 - bounding_box.y1),
shadow_size, shadow_spread);
if (shadow != null)
material.set_layer (0, shadow);
var opacity = actor.get_paint_opacity () * shadow_opacity / 255;
var alpha = Cogl.Color.from_4ub (255, 255, 255, opacity);
alpha.premultiply ();
material.set_color (alpha);
Cogl.set_source (material);
Cogl.rectangle (bounding_box.x1, bounding_box.y1, bounding_box.x2, bounding_box.y2);
actor.continue_paint ();
}
public virtual ActorBox get_bounding_box ()
{
var size = shadow_size * scale_factor;
var bounding_box = ActorBox ();
bounding_box.set_origin (-size, -size);
bounding_box.set_size (actor.width + size * 2, actor.height + size * 2);
return bounding_box;
}
}
}