From 43a2d180755084e1fd2ca2fa4f4a048063a35599 Mon Sep 17 00:00:00 2001 From: Jason Francis Date: Fri, 26 Jul 2019 20:26:37 -0400 Subject: [PATCH] finish up viewport widget with screen previews --- protocol/meson.build | 5 + protocol/wlr-screencopy-unstable-v1.xml | 179 ++++++ resources/head.ui | 2 - resources/wdisplay.ui | 39 +- src/glviewport.c | 135 ++++ src/glviewport.h | 43 ++ src/main.c | 807 ++++++++++++++++++++---- src/meson.build | 6 + src/outputs.c | 349 +++++++++- src/render.c | 360 +++++++++++ src/wdisplay.h | 159 ++++- 11 files changed, 1927 insertions(+), 157 deletions(-) create mode 100644 protocol/wlr-screencopy-unstable-v1.xml create mode 100644 src/glviewport.c create mode 100644 src/glviewport.h create mode 100644 src/render.c diff --git a/protocol/meson.build b/protocol/meson.build index bde9b23..f12853d 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -1,5 +1,8 @@ wayland_scanner = find_program('wayland-scanner') wayland_client = dependency('wayland-client') +wayland_protos = dependency('wayland-protocols', version: '>=1.17') + +wl_protocol_dir = wayland_protos.get_pkgconfig_variable('pkgdatadir') wayland_scanner_code = generator( wayland_scanner, @@ -14,7 +17,9 @@ wayland_scanner_client = generator( ) client_protocols = [ + [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], ['wlr-output-management-unstable-v1.xml'], + ['wlr-screencopy-unstable-v1.xml'] ] client_protos_src = [] diff --git a/protocol/wlr-screencopy-unstable-v1.xml b/protocol/wlr-screencopy-unstable-v1.xml new file mode 100644 index 0000000..a7a2d17 --- /dev/null +++ b/protocol/wlr-screencopy-unstable-v1.xml @@ -0,0 +1,179 @@ + + + + Copyright © 2018 Simon Ser + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows clients to ask the compositor to copy part of the + screen content to a client buffer. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This object is a manager which offers requests to start capturing from a + source. + + + + + Capture the next frame of an entire output. + + + + + + + + + Capture the next frame of an output's region. + + The region is given in output logical coordinates, see + xdg_output.logical_size. The region will be clipped to the output's + extents. + + + + + + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + This object represents a single frame. + + When created, a "buffer" event will be sent. The client will then be able + to send a "copy" request. If the capture is successful, the compositor + will send a "flags" followed by a "ready" event. + + If the capture failed, the "failed" event is sent. This can happen anytime + before the "ready" event. + + Once either a "ready" or a "failed" event is received, the client should + destroy the frame. + + + + + Provides information about the frame's buffer. This event is sent once + as soon as the frame is created. + + The client should then create a buffer with the provided attributes, and + send a "copy" request. + + + + + + + + + + Copy the frame to the supplied buffer. The buffer must have a the + correct size, see zwlr_screencopy_frame_v1.buffer. The buffer needs to + have a supported format. + + If the frame is successfully copied, a "flags" and a "ready" events are + sent. Otherwise, a "failed" event is sent. + + + + + + + + + + + + + + + + Provides flags about the frame. This event is sent once before the + "ready" event. + + + + + + + Called as soon as the frame is copied, indicating it is available + for reading. This event includes the time at which presentation happened + at. + + The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, + each component being an unsigned 32-bit value. Whole seconds are in + tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, + and the additional fractional part in tv_nsec as nanoseconds. Hence, + for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part + may have an arbitrary offset at start. + + After receiving this event, the client should destroy the object. + + + + + + + + + This event indicates that the attempted frame copy has failed. + + After receiving this event, the client should destroy the object. + + + + + + Destroys the frame. This request can be sent at any time by the client. + + + + diff --git a/resources/head.ui b/resources/head.ui index dc0737a..ab424b8 100644 --- a/resources/head.ui +++ b/resources/head.ui @@ -25,13 +25,11 @@ - -16384 16383 1 10 - -16384 16383 1 10 diff --git a/resources/wdisplay.ui b/resources/wdisplay.ui index c156ded..4399cbf 100644 --- a/resources/wdisplay.ui +++ b/resources/wdisplay.ui @@ -3,6 +3,14 @@ + + 1 + 10 + + + 1 + 10 + False @@ -28,6 +36,20 @@ 0 + + + True + True + True + app.capture-screens + Show Screen Contents + + + False + True + 1 + + @@ -112,13 +134,12 @@ True True + canvas_horiz + canvas_vert + 300 + 300 - - True - False - 400 - - + @@ -221,7 +242,7 @@ True True Zoom Out - + True @@ -244,7 +265,7 @@ True True Zoom Reset - + @@ -260,7 +281,7 @@ True True Zoom In - + True diff --git a/src/glviewport.c b/src/glviewport.c new file mode 100644 index 0000000..23a8b60 --- /dev/null +++ b/src/glviewport.c @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2019 cyclopsian + + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "glviewport.h" + +typedef struct _WdGLViewportPrivate { + GtkAdjustment *hadjustment; + GtkAdjustment *vadjustment; + guint hscroll_policy : 1; + guint vscroll_policy : 1; +} WdGLViewportPrivate; + +enum { + PROP_0, + PROP_HADJUSTMENT, + PROP_VADJUSTMENT, + PROP_HSCROLL_POLICY, + PROP_VSCROLL_POLICY +}; + +static void wd_gl_viewport_set_property( + GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); +static void wd_gl_viewport_get_property( + GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); + +G_DEFINE_TYPE_WITH_CODE(WdGLViewport, wd_gl_viewport, GTK_TYPE_GL_AREA, + G_ADD_PRIVATE(WdGLViewport) + G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE, NULL)) + +static void wd_gl_viewport_class_init(WdGLViewportClass *class) { + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + + gobject_class->set_property = wd_gl_viewport_set_property; + gobject_class->get_property = wd_gl_viewport_get_property; + + g_object_class_override_property(gobject_class, PROP_HADJUSTMENT, "hadjustment"); + g_object_class_override_property(gobject_class, PROP_VADJUSTMENT, "vadjustment"); + g_object_class_override_property(gobject_class, PROP_HSCROLL_POLICY, "hscroll-policy"); + g_object_class_override_property(gobject_class, PROP_VSCROLL_POLICY, "vscroll-policy"); +} + +static void viewport_set_adjustment(GtkAdjustment *adjustment, + GtkAdjustment **store) { + if (!adjustment) { + adjustment = gtk_adjustment_new(0., 0., 0., 0., 0., 0.); + } + if (adjustment != *store) { + if (*store != NULL) { + g_object_unref(*store); + } + *store = adjustment; + g_object_ref_sink(adjustment); + } +} + +static void wd_gl_viewport_set_property( + GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { + WdGLViewport *viewport = WD_GL_VIEWPORT(object); + WdGLViewportPrivate *priv = wd_gl_viewport_get_instance_private(viewport); + + switch (prop_id) { + case PROP_HADJUSTMENT: + viewport_set_adjustment(g_value_get_object(value), &priv->hadjustment); + break; + case PROP_VADJUSTMENT: + viewport_set_adjustment(g_value_get_object(value), &priv->vadjustment); + break; + case PROP_HSCROLL_POLICY: + if (priv->hscroll_policy != g_value_get_enum(value)) { + priv->hscroll_policy = g_value_get_enum(value); + g_object_notify_by_pspec(object, pspec); + } + break; + case PROP_VSCROLL_POLICY: + if (priv->vscroll_policy != g_value_get_enum(value)) { + priv->vscroll_policy = g_value_get_enum(value); + g_object_notify_by_pspec (object, pspec); + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void wd_gl_viewport_get_property( + GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { + WdGLViewport *viewport = WD_GL_VIEWPORT(object); + WdGLViewportPrivate *priv = wd_gl_viewport_get_instance_private(viewport); + + switch (prop_id) { + case PROP_HADJUSTMENT: + g_value_set_object(value, priv->hadjustment); + break; + case PROP_VADJUSTMENT: + g_value_set_object(value, priv->vadjustment); + break; + case PROP_HSCROLL_POLICY: + g_value_set_enum(value, priv->hscroll_policy); + break; + case PROP_VSCROLL_POLICY: + g_value_set_enum(value, priv->vscroll_policy); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void wd_gl_viewport_init(WdGLViewport *viewport) { +} + +GtkWidget *wd_gl_viewport_new(void) { + return gtk_widget_new(WD_TYPE_GL_VIEWPORT, NULL); +} diff --git a/src/glviewport.h b/src/glviewport.h new file mode 100644 index 0000000..f82f4f4 --- /dev/null +++ b/src/glviewport.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2019 cyclopsian + + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef WDISPLAY_GLVIEWPORT_H +#define WDISPLAY_GLVIEWPORT_H + +#include + +G_BEGIN_DECLS + +#define WD_TYPE_GL_VIEWPORT (wd_gl_viewport_get_type()) +G_DECLARE_DERIVABLE_TYPE( + WdGLViewport, wd_gl_viewport, WD, GL_VIEWPORT,GtkGLArea) + +struct _WdGLViewportClass { + GtkGLAreaClass parent_class; +}; + +GtkWidget *wd_gl_viewport_new(void); + +G_END_DECLS + +#endif diff --git a/src/main.c b/src/main.c index c8b5db9..ac3daa8 100644 --- a/src/main.c +++ b/src/main.c @@ -25,6 +25,7 @@ #include #include "wdisplay.h" +#include "glviewport.h" __attribute__((noreturn)) void wd_fatal_error(int status, const char *message) { GtkWindow *parent = gtk_application_get_active_window(GTK_APPLICATION(g_application_get_default())); @@ -180,6 +181,208 @@ static gboolean apply_done_reset(gpointer data) { return FALSE; } +static void update_scroll_size(struct wd_state *state) { + state->render.viewport_width = gtk_widget_get_allocated_width(state->canvas); + state->render.viewport_height = gtk_widget_get_allocated_height(state->canvas); + + GtkAdjustment *scroll_x_adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + GtkAdjustment *scroll_y_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + int scroll_x_upper = state->render.width; + int scroll_y_upper = state->render.height; + gtk_adjustment_set_upper(scroll_x_adj, MAX(0, scroll_x_upper)); + gtk_adjustment_set_upper(scroll_y_adj, MAX(0, scroll_y_upper)); + gtk_adjustment_set_page_size(scroll_x_adj, state->render.viewport_width); + gtk_adjustment_set_page_size(scroll_y_adj, state->render.viewport_height); + gtk_adjustment_set_page_increment(scroll_x_adj, state->render.viewport_width); + gtk_adjustment_set_page_increment(scroll_y_adj, state->render.viewport_height); + gtk_adjustment_set_step_increment(scroll_x_adj, state->render.viewport_width / 10); + gtk_adjustment_set_step_increment(scroll_y_adj, state->render.viewport_height / 10); +} + +/* + * Recalculates the desired canvas size, accounting for zoom + margins. + */ +static void update_canvas_size(struct wd_state *state) { + int xmin = 0; + int xmax = 0; + int ymin = 0; + int ymax = 0; + + struct wd_head *head; + wl_list_for_each(head, &state->heads, link) { + int w = head->custom_mode.width; + int h = head->custom_mode.height; + if (head->enabled && head->mode != NULL) { + w = head->mode->width; + h = head->mode->height; + } + if (head->scale > 0.) { + w /= head->scale; + h /= head->scale; + } + + int x2 = head->x + w; + int y2 = head->y + h; + xmin = MIN(xmin, head->x); + xmax = MAX(xmax, x2); + ymin = MIN(ymin, head->y); + ymax = MAX(ymax, y2); + } + // update canvas sizings + state->render.x_origin = floor(xmin * state->zoom) - CANVAS_MARGIN; + state->render.y_origin = floor(ymin * state->zoom) - CANVAS_MARGIN; + state->render.width = ceil((xmax - xmin) * state->zoom) + CANVAS_MARGIN * 2; + state->render.height = ceil((ymax - ymin) * state->zoom) + CANVAS_MARGIN * 2; + + update_scroll_size(state); +} + +static void cache_scroll(struct wd_state *state) { + GtkAdjustment *scroll_x_adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + GtkAdjustment *scroll_y_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + state->render.scroll_x = gtk_adjustment_get_value(scroll_x_adj); + state->render.scroll_y = gtk_adjustment_get_value(scroll_y_adj); +} + +static gboolean redraw_canvas(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer data); + +static void update_tick_callback(struct wd_state *state) { + bool any_animate = false; + for (int i = 0; i < state->render.head_count; i++) { + struct wd_render_head_data *head = &state->render.heads[i]; + if (state->render.updated_at < head->transition_begin + HOVER_USECS) { + any_animate = true; + break; + } + } + if (!any_animate && !state->capture) { + if (state->canvas_tick != -1) { + gtk_widget_remove_tick_callback(state->canvas, state->canvas_tick); + state->canvas_tick = -1; + } + } else if (state->canvas_tick == -1) { + state->canvas_tick = + gtk_widget_add_tick_callback(state->canvas, redraw_canvas, state, NULL); + } + gtk_gl_area_queue_render(GTK_GL_AREA(state->canvas)); + gtk_gl_area_set_auto_render(GTK_GL_AREA(state->canvas), state->capture); +} + +static void update_cursor(struct wd_state *state) { + bool any_hovered = false; + struct wd_head *head; + wl_list_for_each(head, &state->heads, link) { + struct wd_render_head_data *render = head->render; + if (render != NULL && render->hovered) { + any_hovered = true; + break; + } + } + GdkWindow *window = gtk_widget_get_window(state->canvas); + if (any_hovered) { + gdk_window_set_cursor(window, state->grab_cursor); + } else if (state->clicked != NULL) { + gdk_window_set_cursor(window, state->grabbing_cursor); + } else if (state->panning) { + gdk_window_set_cursor(window, state->move_cursor); + } else { + gdk_window_set_cursor(window, NULL); + } +} + +static void update_hovered(struct wd_state *state) { + GdkDisplay *display = gdk_display_get_default(); + GdkWindow *window = gtk_widget_get_window(state->canvas); + if (!gtk_widget_get_realized(state->canvas)) { + return; + } + GdkFrameClock *clock = gtk_widget_get_frame_clock(state->canvas); + uint64_t tick = gdk_frame_clock_get_frame_time(clock); + g_autoptr(GList) seats = gdk_display_list_seats(display); + struct wd_head *head; + wl_list_for_each(head, &state->heads, link) { + struct wd_render_head_data *render = head->render; + if (render != NULL) { + bool init_hovered = render->hovered; + render->hovered = false; + if (state->clicked == head) { + render->hovered = true; + } else if (state->clicked == NULL) { + for (GList *iter = seats; iter != NULL; iter = iter->next) { + double mouse_x; + double mouse_y; + + GdkDevice *pointer = gdk_seat_get_pointer(GDK_SEAT(iter->data)); + gdk_window_get_device_position_double(window, pointer, &mouse_x, &mouse_y, NULL); + if (mouse_x >= render->x1 && mouse_x < render->x2 && + mouse_y >= render->y1 && mouse_y < render->y2) { + render->hovered = true; + break; + } + } + } + if (init_hovered != render->hovered) { + render->transition_begin = tick; + } + } + } + update_cursor(state); + update_tick_callback(state); +} + +static inline void color_to_float_array(GtkStyleContext *ctx, + const char *color_name, float out[4]) { + GdkRGBA color; + gtk_style_context_lookup_color(ctx, color_name, &color); + out[0] = color.red; + out[1] = color.green; + out[2] = color.blue; + out[3] = color.alpha; +} + +static void queue_canvas_draw(struct wd_state *state) { + GtkStyleContext *style_ctx = gtk_widget_get_style_context(state->canvas); + color_to_float_array(style_ctx, + "theme_fg_color", state->render.fg_color); + color_to_float_array(style_ctx, + "theme_bg_color", state->render.bg_color); + color_to_float_array(style_ctx, + "borders", state->render.border_color); + color_to_float_array(style_ctx, + "theme_selected_bg_color", state->render.selection_color); + + cache_scroll(state); + + state->render.head_count = 0; + g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); + for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { + GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder")); + gboolean enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled"))); + if (enabled) { + int x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_x"))); + int y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_y"))); + int w = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width"))); + int h = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height"))); + double scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale"))); + if (scale <= 0.) + scale = 1.; + + struct wd_head *head = g_object_get_data(G_OBJECT(form_iter->data), "head"); + struct wd_render_head_data *render = &state->render.heads[state->render.head_count]; + render->x1 = floor(x * state->zoom - state->render.scroll_x - state->render.x_origin); + render->y1 = floor(y * state->zoom - state->render.scroll_y - state->render.y_origin); + render->x2 = floor(render->x1 + w * state->zoom / scale); + render->y2 = floor(render->y1 + h * state->zoom / scale); + head->render = render; + + state->render.head_count++; + if (state->render.head_count >= HEADS_MAX) + break; + } + } + gtk_gl_area_queue_render(GTK_GL_AREA(state->canvas)); +} + // BEGIN FORM CALLBACKS static void show_apply(struct wd_state *state) { const gchar *page = "title"; @@ -191,7 +394,12 @@ static void show_apply(struct wd_state *state) { } } gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), page); - gtk_widget_queue_draw(state->canvas); +} + +static void update_ui(struct wd_state *state) { + show_apply(state); + update_canvas_size(state); + queue_canvas_draw(state); } static void update_sensitivity(GtkWidget *form) { @@ -226,7 +434,7 @@ static void select_rotate_option(GtkWidget *form, GtkWidget *model_button) { static void rotate_selected(GSimpleAction *action, GVariant *param, gpointer data) { select_rotate_option(GTK_WIDGET(data), g_object_get_data(G_OBJECT(action), "widget")); const struct wd_head *head = g_object_get_data(G_OBJECT(data), "head"); - show_apply(head->state); + update_ui(head->state); } static void select_mode_option(GtkWidget *form, int32_t w, int32_t h, int32_t r) { @@ -257,47 +465,10 @@ static void mode_selected(GSimpleAction *action, GVariant *param, gpointer data) update_mode_entries(form, mode->width, mode->height, mode->refresh); select_mode_option(form, mode->width, mode->height, mode->refresh); - show_apply(head->state); + update_ui(head->state); } // END FORM CALLBACKS -/* - * Recalculates the desired canvas size, accounting for zoom + margins. - */ -static void update_canvas_size(struct wd_state *state) { - int xmin = 0; - int xmax = 0; - int ymin = 0; - int ymax = 0; - - struct wd_head *head; - wl_list_for_each(head, &state->heads, link) { - int w = head->custom_mode.width; - int h = head->custom_mode.height; - if (head->enabled && head->mode != NULL) { - w = head->mode->width; - h = head->mode->height; - } - if (head->scale > 0.) { - w /= head->scale; - h /= head->scale; - } - - int x2 = head->x + w; - int y2 = head->y + h; - xmin = MIN(xmin, head->x); - xmax = MAX(xmax, x2); - ymin = MIN(ymin, head->y); - ymax = MAX(ymax, y2); - } - // update canvas sizings - state->xorigin = floor(xmin * state->zoom) - CANVAS_MARGIN; - state->yorigin = floor(ymin * state->zoom) - CANVAS_MARGIN; - int heads_width = ceil((xmax - xmin) * state->zoom) + CANVAS_MARGIN * 2; - int heads_height = ceil((ymax - ymin) * state->zoom) + CANVAS_MARGIN * 2; - gtk_layout_set_size(GTK_LAYOUT(state->canvas), heads_width, heads_height); -} - static void clear_menu(GtkWidget *box, GActionMap *action_map) { g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(box)); for (GList *child = children; child != NULL; child = child->next) { @@ -305,6 +476,7 @@ static void clear_menu(GtkWidget *box, GActionMap *action_map) { gtk_container_remove(GTK_CONTAINER(box), GTK_WIDGET(child->data)); } } + static void update_head_form(GtkWidget *form, unsigned int fields) { GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); GtkWidget *description = GTK_WIDGET(gtk_builder_get_object(builder, "description")); @@ -368,7 +540,18 @@ static void update_head_form(GtkWidget *form, unsigned int fields) { w = head->mode->width; h = head->mode->height; r = head->mode->refresh; + } else if (!head->enabled && w == 0 && h == 0) { + struct wd_mode *mode; + wl_list_for_each(mode, &head->modes, link) { + if (mode->preferred) { + w = mode->width; + h = mode->height; + r = mode->refresh; + break; + } + } } + update_mode_entries(form, w, h, r); select_mode_option(form, w, h, r); gtk_widget_show_all(mode_box); @@ -389,8 +572,7 @@ static void update_head_form(GtkWidget *form, unsigned int fields) { if (fields & WD_FIELD_ENABLED) { update_sensitivity(form); } - show_apply(head->state); - gtk_widget_queue_draw(head->state->canvas); + update_ui(head->state); } void wd_ui_reset_heads(struct wd_state *state) { @@ -437,14 +619,14 @@ void wd_ui_reset_heads(struct wd_state *state) { gtk_widget_show_all(form); g_signal_connect_swapped(gtk_builder_get_object(builder, "enabled"), "toggled", G_CALLBACK(update_sensitivity), form); - g_signal_connect_swapped(gtk_builder_get_object(builder, "enabled"), "toggled", G_CALLBACK(show_apply), state); - g_signal_connect_swapped(gtk_builder_get_object(builder, "scale"), "value-changed", G_CALLBACK(show_apply), state); - g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_x"), "value-changed", G_CALLBACK(show_apply), state); - g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_y"), "value-changed", G_CALLBACK(show_apply), state); - g_signal_connect_swapped(gtk_builder_get_object(builder, "width"), "value-changed", G_CALLBACK(show_apply), state); - g_signal_connect_swapped(gtk_builder_get_object(builder, "height"), "value-changed", G_CALLBACK(show_apply), state); - g_signal_connect_swapped(gtk_builder_get_object(builder, "refresh"), "value-changed", G_CALLBACK(show_apply), state); - g_signal_connect_swapped(gtk_builder_get_object(builder, "flipped"), "toggled", G_CALLBACK(show_apply), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "enabled"), "toggled", G_CALLBACK(update_ui), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "scale"), "value-changed", G_CALLBACK(update_ui), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_x"), "value-changed", G_CALLBACK(update_ui), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_y"), "value-changed", G_CALLBACK(update_ui), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "width"), "value-changed", G_CALLBACK(update_ui), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "height"), "value-changed", G_CALLBACK(update_ui), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "refresh"), "value-changed", G_CALLBACK(update_ui), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "flipped"), "toggled", G_CALLBACK(update_ui), state); } else { form = form_iter->data; @@ -458,13 +640,10 @@ void wd_ui_reset_heads(struct wd_state *state) { g_object_unref(builder); gtk_container_remove(GTK_CONTAINER(state->stack), GTK_WIDGET(form_iter->data)); } - gtk_widget_queue_draw(state->canvas); + update_canvas_size(state); + queue_canvas_draw(state); } -/* - * Updates the UI form for a single head. Useful for when the compositor notifies us of - * updated configuration caused by another program. - */ void wd_ui_reset_head(const struct wd_head *head, unsigned int fields) { if (head->state->stack == NULL) { return; @@ -474,8 +653,11 @@ void wd_ui_reset_head(const struct wd_head *head, unsigned int fields) { const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head"); if (head == other) { update_head_form(GTK_WIDGET(form_iter->data), fields); + break; } } + update_canvas_size(head->state); + queue_canvas_draw(head->state); } void wd_ui_reset_all(struct wd_state *state) { @@ -484,6 +666,8 @@ void wd_ui_reset_all(struct wd_state *state) { for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { update_head_form(GTK_WIDGET(form_iter->data), WD_FIELDS_ALL); } + update_canvas_size(state); + queue_canvas_draw(state); } void wd_ui_apply_done(struct wd_state *state, struct wl_list *outputs) { @@ -509,51 +693,398 @@ void wd_ui_show_error(struct wd_state *state, const char *message) { } // BEGIN GLOBAL CALLBACKS -static void cleanup(GtkWidget *window, gpointer state) { - g_free(state); +static void cleanup(GtkWidget *window, gpointer data) { + struct wd_state *state = data; + g_object_unref(state->grab_cursor); + g_object_unref(state->grabbing_cursor); + g_object_unref(state->move_cursor); + wd_state_destroy(state); } -gboolean draw(GtkWidget *widget, cairo_t *cr, gpointer data) { +static void monitor_added(GdkDisplay *display, GdkMonitor *monitor, gpointer data) { + wd_add_output(data, gdk_wayland_monitor_get_wl_output(monitor)); +} + +static void monitor_removed(GdkDisplay *display, GdkMonitor *monitor, gpointer data) { + struct wl_display *wl_display = gdk_wayland_display_get_wl_display(display); + wd_remove_output(data, gdk_wayland_monitor_get_wl_output(monitor), wl_display); +} + +static void canvas_realize(GtkWidget *widget, gpointer data) { + gtk_gl_area_make_current(GTK_GL_AREA(widget)); + if (gtk_gl_area_get_error(GTK_GL_AREA(widget)) != NULL) { + return; + } + struct wd_state *state = data; + state->gl_data = wd_gl_setup(); +} + +static inline bool size_changed(const struct wd_render_head_data *render) { + return render->x2 - render->x1 != render->tex_width || + render->y2 - render->y1 != render->tex_height; +} + +static inline void cairo_set_source_color(cairo_t *cr, float color[4]) { + cairo_set_source_rgba(cr, color[0], color[1], color[2], color[3]); +} + +static void update_zoom(struct wd_state *state) { + g_autofree gchar *zoom_percent = g_strdup_printf("%.f%%", state->zoom * 100.); + gtk_button_set_label(GTK_BUTTON(state->zoom_reset), zoom_percent); + gtk_widget_set_sensitive(state->zoom_in, state->zoom < MAX_ZOOM); + gtk_widget_set_sensitive(state->zoom_out, state->zoom > MIN_ZOOM); + update_canvas_size(state); - GtkStyleContext *style_ctx = gtk_widget_get_style_context(widget); - GtkAdjustment *scroll_x_adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); - GtkAdjustment *scroll_y_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); - double scroll_x = gtk_adjustment_get_value(scroll_x_adj); - double scroll_y = gtk_adjustment_get_value(scroll_y_adj); - int width = gtk_widget_get_allocated_width(widget); - int height = gtk_widget_get_allocated_height(widget); + queue_canvas_draw(state); +} - GdkRGBA border; - gtk_style_context_lookup_color(style_ctx, "borders", &border); +static void zoom_to(struct wd_state *state, double zoom) { + state->zoom = zoom; + state->zoom = MAX(state->zoom, MIN_ZOOM); + state->zoom = MIN(state->zoom, MAX_ZOOM); + update_zoom(state); +} - gdk_cairo_set_source_rgba(cr, &border); - cairo_set_line_width(cr, .5); +static void zoom_out(struct wd_state *state) { + zoom_to(state, state->zoom * 0.75); +} - gtk_render_background(style_ctx, cr, 0, 0, width, height); - g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); - for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { - GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder")); - gboolean enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled"))); - if (enabled) { - int x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_x"))); - int y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_y"))); - int w = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width"))); - int h = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height"))); - double scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale"))); - if (scale <= 0.) scale = 1.; - cairo_rectangle(cr, - x * state->zoom + .5 - scroll_x - state->xorigin, - y * state->zoom + .5 - scroll_y - state->yorigin, - w * state->zoom / scale, - h * state->zoom / scale); - cairo_stroke(cr); +static void zoom_reset(struct wd_state *state) { + zoom_to(state, DEFAULT_ZOOM); +} + +static void zoom_in(struct wd_state *state) { + zoom_to(state, state->zoom / 0.75); +} + +#define TEXT_MARGIN 5 + +static cairo_surface_t *draw_head(PangoContext *pango, + struct wd_render_data *info, const char *name, + unsigned width, unsigned height) { + cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + width, height); + cairo_t *cr = cairo_create(surface); + + cairo_rectangle(cr, 0., 0., width, height); + cairo_set_source_color(cr, info->border_color); + cairo_fill(cr); + + cairo_set_line_width(cr, 1.); + cairo_rectangle(cr, 0, 0, width, height); + cairo_set_source_color(cr, info->fg_color); + cairo_stroke(cr); + + PangoLayout *layout = pango_layout_new(pango); + pango_layout_set_text(layout, name, -1); + int text_width = pango_units_from_double(width - TEXT_MARGIN * 2); + int text_height = pango_units_from_double(height - TEXT_MARGIN * 2); + pango_layout_set_width(layout, MAX(text_width, 0)); + pango_layout_set_height(layout, MAX(text_height, 0)); + pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); + + cairo_set_source_color(cr, info->fg_color); + pango_layout_get_size(layout, &text_width, &text_height); + cairo_move_to(cr, TEXT_MARGIN, (height - PANGO_PIXELS(text_height)) / 2); + pango_cairo_show_layout(cr, layout); + g_object_unref(layout); + + cairo_destroy(cr); + cairo_surface_flush(surface); + return surface; +} + +static void canvas_render(GtkGLArea *area, GdkGLContext *context, gpointer data) { + struct wd_state *state = data; + + PangoContext *pango = gtk_widget_get_pango_context(state->canvas); + GdkFrameClock *clock = gtk_widget_get_frame_clock(state->canvas); + uint64_t tick = gdk_frame_clock_get_frame_time(clock); + + wd_capture_frame(state); + + struct wd_head *head; + wl_list_for_each(head, &state->heads, link) { + struct wd_render_head_data *render = head->render; + struct wd_output *output = wd_find_output(state, head); + struct wd_frame *frame = NULL; + if (output != NULL && !wl_list_empty(&output->frames)) { + frame = wl_container_of(output->frames.prev, frame, link); + } + if (render != NULL) { + if (state->capture && frame != NULL && frame->pixels != NULL) { + if (frame->tick > render->updated_at) { + render->tex_stride = frame->stride; + render->tex_width = frame->width; + render->tex_height = frame->height; + render->pixels = frame->pixels; + render->preview = true; + render->updated_at = tick; + render->y_invert = frame->y_invert; + } + } else if (render->preview + || render->pixels == NULL || size_changed(render)) { + render->tex_width = render->x2 - render->x1; + render->tex_height = render->y2 - render->y1; + render->preview = false; + if (head->surface != NULL) { + cairo_surface_destroy(head->surface); + } + head->surface = draw_head(pango, &state->render, head->name, + render->tex_width, render->tex_height); + render->pixels = cairo_image_surface_get_data(head->surface); + render->tex_stride = cairo_image_surface_get_stride(head->surface); + render->updated_at = tick; + render->y_invert = false; + } } } + wd_gl_render(state->gl_data, &state->render, tick); + state->render.updated_at = tick; +} + +static void canvas_unrealize(GtkWidget *widget, gpointer data) { + gtk_gl_area_make_current(GTK_GL_AREA(widget)); + if (gtk_gl_area_get_error(GTK_GL_AREA(widget)) != NULL) { + return; + } + struct wd_state *state = data; + + GdkDisplay *gdk_display = gdk_display_get_default(); + struct wl_display *display = gdk_wayland_display_get_wl_display(gdk_display); + wd_capture_wait(state, display); + + wd_gl_cleanup(state->gl_data); + state->gl_data = NULL; +} + +static gboolean canvas_click(GtkWidget *widget, GdkEvent *event, + gpointer data) { + struct wd_state *state = data; + if (event->button.type == GDK_BUTTON_PRESS) { + if (event->button.button == 1) { + int i = 0; + struct wd_head *head; + wl_list_for_each(head, &state->heads, link) { + struct wd_render_head_data *render = head->render; + if (render != NULL) { + double mouse_x = event->button.x; + double mouse_y = event->button.y; + if (mouse_x >= render->x1 && mouse_x < render->x2 && + mouse_y >= render->y1 && mouse_y < render->y2) { + state->clicked = head; + state->click_offset.x = event->button.x - render->x1; + state->click_offset.y = event->button.y - render->y1; + break; + } + } + i++; + } + if (state->clicked != NULL) { + g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); + for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { + const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head"); + if (state->clicked == other) { + gtk_stack_set_visible_child(GTK_STACK(state->stack), form_iter->data); + break; + } + } + } + } else if (event->button.button == 2) { + state->panning = true; + state->pan_last.x = event->button.x; + state->pan_last.y = event->button.y; + } + } return TRUE; } +static gboolean canvas_release(GtkWidget *widget, GdkEvent *event, + gpointer data) { + struct wd_state *state = data; + if (event->button.button == 1) { + state->clicked = NULL; + } + if (event->button.button == 2) { + state->panning = false; + } + update_cursor(state); + return TRUE; +} + +#define SNAP_DIST 6. + +static gboolean canvas_motion(GtkWidget *widget, GdkEvent *event, + gpointer data) { + struct wd_state *state = data; + if (event->motion.state & GDK_BUTTON2_MASK) { + GtkAdjustment *xadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + GtkAdjustment *yadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + double delta_x = event->motion.x - state->pan_last.x; + double delta_y = event->motion.y - state->pan_last.y; + gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) + delta_x); + gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) + delta_y); + state->pan_last.x = event->motion.x; + state->pan_last.y = event->motion.y; + queue_canvas_draw(state); + } + if ((event->motion.state & GDK_BUTTON1_MASK) && state->clicked != NULL) { + GtkWidget *form = NULL; + g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); + for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { + const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head"); + if (state->clicked == other) { + form = form_iter->data; + break; + } + } + if (form != NULL) { + GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); + struct wd_point size = { + .x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width"))), + .y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height"))), + }; + struct wd_point tl = { + .x = (event->motion.x - state->click_offset.x + + state->render.x_origin + state->render.scroll_x) / state->zoom, + .y = (event->motion.y - state->click_offset.y + + state->render.y_origin + state->render.scroll_y) / state->zoom + }; + const struct wd_point br = { + .x = tl.x + size.x, + .y = tl.y + size.y + }; + struct wd_point new_pos = tl; + float snap = SNAP_DIST / state->zoom; + + for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { + const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head"); + if (other != state->clicked && !(event->motion.state & GDK_SHIFT_MASK)) { + GtkBuilder *other_builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder")); + double x1 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "pos_x"))); + double y1 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "pos_y"))); + double x2 = x1 + gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "width"))); + double y2 = y1 + gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "height"))); + if (fabs(br.x) <= snap) + new_pos.x = -size.x; + if (fabs(br.y) <= snap) + new_pos.y = -size.y; + if (fabs(br.x - x1) <= snap) + new_pos.x = x1 - size.x; + if (fabs(br.x - x2) <= snap) + new_pos.x = x2 - size.x; + if (fabs(br.y - y1) <= snap) + new_pos.y = y1 - size.y; + if (fabs(br.y - y2) <= snap) + new_pos.y = y2 - size.y; + + if (fabs(tl.x) <= snap) + new_pos.x = 0.; + if (fabs(tl.y) <= snap) + new_pos.y = 0.; + if (fabs(tl.x - x1) <= snap) + new_pos.x = x1; + if (fabs(tl.x - x2) <= snap) + new_pos.x = x2; + if (fabs(tl.y - y1) <= snap) + new_pos.y = y1; + if (fabs(tl.y - y2) <= snap) + new_pos.y = y2; + } + } + GtkWidget *pos_x = GTK_WIDGET(gtk_builder_get_object(builder, "pos_x")); + GtkWidget *pos_y = GTK_WIDGET(gtk_builder_get_object(builder, "pos_y")); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(pos_x), new_pos.x); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(pos_y), new_pos.y); + } + } + update_hovered(state); + return TRUE; +} + +static gboolean canvas_enter(GtkWidget *widget, GdkEvent *event, + gpointer data) { + struct wd_state *state = data; + if (!(event->crossing.state & GDK_BUTTON1_MASK)) { + state->clicked = NULL; + } + if (!(event->crossing.state & GDK_BUTTON2_MASK)) { + state->panning = false; + } + update_cursor(state); + return TRUE; +} + +static gboolean canvas_leave(GtkWidget *widget, GdkEvent *event, + gpointer data) { + struct wd_state *state = data; + for (int i = 0; i < state->render.head_count; i++) { + struct wd_render_head_data *head = &state->render.heads[i]; + head->hovered = false; + } + update_tick_callback(state); + return TRUE; +} + +static gboolean canvas_scroll(GtkWidget *widget, GdkEvent *event, + gpointer data) { + struct wd_state *state = data; + if (event->scroll.state & GDK_CONTROL_MASK) { + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + zoom_in(state); + break; + case GDK_SCROLL_DOWN: + zoom_out(state); + break; + case GDK_SCROLL_SMOOTH: + if (event->scroll.delta_y) + zoom_to(state, state->zoom * pow(0.75, event->scroll.delta_y)); + break; + default: + break; + } + } else { + GtkAdjustment *xadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + GtkAdjustment *yadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + double xstep = gtk_adjustment_get_step_increment(xadj); + double ystep = gtk_adjustment_get_step_increment(yadj); + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) - ystep); + break; + case GDK_SCROLL_DOWN: + gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) + ystep); + break; + case GDK_SCROLL_LEFT: + gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) - xstep); + break; + case GDK_SCROLL_RIGHT: + gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) + xstep); + break; + case GDK_SCROLL_SMOOTH: + if (event->scroll.delta_x) + gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) + xstep * event->scroll.delta_x); + if (event->scroll.delta_y) + gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) + ystep * event->scroll.delta_y); + break; + default: + break; + } + } + return FALSE; +} + +static void canvas_resize(GtkWidget *widget, GdkRectangle *allocation, + gpointer data) { + struct wd_state *state = data; + update_scroll_size(state); +} + static void cancel_changes(GtkButton *button, gpointer data) { struct wd_state *state = data; gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), "title"); @@ -564,34 +1095,6 @@ static void apply_changes(GtkButton *button, gpointer data) { apply_state(data); } -static void update_zoom(struct wd_state *state) { - g_autofree gchar *zoom_percent = g_strdup_printf("%.f%%", state->zoom * 100.); - gtk_button_set_label(GTK_BUTTON(state->zoom_reset), zoom_percent); - gtk_widget_set_sensitive(state->zoom_in, state->zoom < MAX_ZOOM); - gtk_widget_set_sensitive(state->zoom_out, state->zoom > MIN_ZOOM); - gtk_widget_queue_draw(state->canvas); -} - -static void zoom_out(GtkButton *button, gpointer data) { - struct wd_state *state = data; - state->zoom *= 0.75; - state->zoom = MAX(state->zoom, MIN_ZOOM); - update_zoom(state); -} - -static void zoom_reset(GtkButton *button, gpointer data) { - struct wd_state *state = data; - state->zoom = DEFAULT_ZOOM; - update_zoom(state); -} - -static void zoom_in(GtkButton *button, gpointer data) { - struct wd_state *state = data; - state->zoom /= 0.75; - state->zoom = MIN(state->zoom, MAX_ZOOM); - update_zoom(state); -} - static void info_response(GtkInfoBar *info_bar, gint response_id, gpointer data) { gtk_info_bar_set_revealed(info_bar, FALSE); } @@ -610,28 +1113,47 @@ static void auto_apply_selected(GSimpleAction *action, GVariant *param, gpointer g_simple_action_set_state(action, g_variant_new_boolean(state->autoapply)); } +static gboolean redraw_canvas(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer data) { + struct wd_state *state = data; + if (state->capture) { + wd_capture_frame(state); + } + queue_canvas_draw(state); + return G_SOURCE_CONTINUE; +} + +static void capture_selected(GSimpleAction *action, GVariant *param, gpointer data) { + struct wd_state *state = data; + state->capture = !state->capture; + g_simple_action_set_state(action, g_variant_new_boolean(state->capture)); + update_tick_callback(state); +} + static void activate(GtkApplication* app, gpointer user_data) { GdkDisplay *gdk_display = gdk_display_get_default(); if (!GDK_IS_WAYLAND_DISPLAY(gdk_display)) { wd_fatal_error(1, "This program is only usable on Wayland sessions."); } - struct wd_state *state = g_new0(struct wd_state, 1); + struct wd_state *state = wd_state_create(); state->zoom = DEFAULT_ZOOM; - wl_list_init(&state->heads); + state->canvas_tick = -1; GtkCssProvider *css_provider = gtk_css_provider_new(); gtk_css_provider_load_from_resource(css_provider, "/style.css"); gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), GTK_STYLE_PROVIDER(css_provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + state->grab_cursor = gdk_cursor_new_from_name(gdk_display, "grab"); + state->grabbing_cursor = gdk_cursor_new_from_name(gdk_display, "grabbing"); + state->move_cursor = gdk_cursor_new_from_name(gdk_display, "move"); + GtkBuilder *builder = gtk_builder_new_from_resource("/wdisplay.ui"); GtkWidget *window = GTK_WIDGET(gtk_builder_get_object(builder, "heads_window")); state->header_stack = GTK_WIDGET(gtk_builder_get_object(builder, "header_stack")); state->stack_switcher = GTK_WIDGET(gtk_builder_get_object(builder, "heads_stack_switcher")); state->stack = GTK_WIDGET(gtk_builder_get_object(builder, "heads_stack")); state->scroller = GTK_WIDGET(gtk_builder_get_object(builder, "heads_scroll")); - state->canvas = GTK_WIDGET(gtk_builder_get_object(builder, "heads_layout")); state->spinner = GTK_WIDGET(gtk_builder_get_object(builder, "spinner")); state->zoom_out = GTK_WIDGET(gtk_builder_get_object(builder, "zoom_out")); state->zoom_reset = GTK_WIDGET(gtk_builder_get_object(builder, "zoom_reset")); @@ -640,7 +1162,7 @@ static void activate(GtkApplication* app, gpointer user_data) { state->info_bar = GTK_WIDGET(gtk_builder_get_object(builder, "heads_info")); state->info_label = GTK_WIDGET(gtk_builder_get_object(builder, "heads_info_label")); state->menu_button = GTK_WIDGET(gtk_builder_get_object(builder, "menu_button")); - gtk_builder_add_callback_symbol(builder, "heads_draw", G_CALLBACK(draw)); + gtk_builder_add_callback_symbol(builder, "apply_changes", G_CALLBACK(apply_changes)); gtk_builder_add_callback_symbol(builder, "cancel_changes", G_CALLBACK(cancel_changes)); gtk_builder_add_callback_symbol(builder, "zoom_out", G_CALLBACK(zoom_out)); @@ -650,6 +1172,31 @@ static void activate(GtkApplication* app, gpointer user_data) { gtk_builder_add_callback_symbol(builder, "destroy", G_CALLBACK(cleanup)); gtk_builder_connect_signals(builder, state); gtk_box_set_homogeneous(GTK_BOX(gtk_builder_get_object(builder, "zoom_box")), FALSE); + + state->canvas = wd_gl_viewport_new(); + gtk_container_add(GTK_CONTAINER(state->scroller), state->canvas); + gtk_widget_add_events(state->canvas, GDK_POINTER_MOTION_MASK + | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_SCROLL_MASK + | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK); + g_signal_connect(state->canvas, "realize", G_CALLBACK(canvas_realize), state); + g_signal_connect(state->canvas, "render", G_CALLBACK(canvas_render), state); + g_signal_connect(state->canvas, "unrealize", G_CALLBACK(canvas_unrealize), state); + g_signal_connect(state->canvas, "button-press-event", G_CALLBACK(canvas_click), state); + g_signal_connect(state->canvas, "button-release-event", G_CALLBACK(canvas_release), state); + g_signal_connect(state->canvas, "enter-notify-event", G_CALLBACK(canvas_enter), state); + g_signal_connect(state->canvas, "leave-notify-event", G_CALLBACK(canvas_leave), state); + g_signal_connect(state->canvas, "motion-notify-event", G_CALLBACK(canvas_motion), state); + g_signal_connect(state->canvas, "scroll-event", G_CALLBACK(canvas_scroll), state); + g_signal_connect(state->canvas, "size-allocate", G_CALLBACK(canvas_resize), state); + gtk_gl_area_set_use_es(GTK_GL_AREA(state->canvas), TRUE); + gtk_gl_area_set_has_alpha(GTK_GL_AREA(state->canvas), TRUE); + gtk_gl_area_set_auto_render(GTK_GL_AREA(state->canvas), state->capture); + + GtkAdjustment *scroll_x_adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + GtkAdjustment *scroll_y_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + g_signal_connect_swapped(scroll_x_adj, "value-changed", G_CALLBACK(queue_canvas_draw), state); + g_signal_connect_swapped(scroll_y_adj, "value-changed", G_CALLBACK(queue_canvas_draw), state); + update_zoom(state); GSimpleActionGroup *main_actions = g_simple_action_group_new(); @@ -661,6 +1208,11 @@ static void activate(GtkApplication* app, gpointer user_data) { g_signal_connect(autoapply_action, "activate", G_CALLBACK(auto_apply_selected), state); g_action_map_add_action(G_ACTION_MAP(main_actions), G_ACTION(autoapply_action)); + GSimpleAction *capture_action = g_simple_action_new_stateful("capture-screens", NULL, + g_variant_new_boolean(state->capture)); + g_signal_connect(capture_action, "activate", G_CALLBACK(capture_selected), state); + g_action_map_add_action(G_ACTION_MAP(main_actions), G_ACTION(capture_action)); + /* first child of GtkInfoBar is always GtkRevealer */ g_autoptr(GList) info_children = gtk_container_get_children(GTK_CONTAINER(state->info_bar)); g_signal_connect(info_children->data, "notify::child-revealed", G_CALLBACK(info_bar_animation_done), state); @@ -671,6 +1223,23 @@ static void activate(GtkApplication* app, gpointer user_data) { if (state->output_manager == NULL) { wd_fatal_error(1, "Compositor doesn't support wlr-output-management-unstable-v1"); } + if (state->xdg_output_manager == NULL) { + wd_fatal_error(1, "Compositor doesn't support xdg-output-unstable-v1"); + } + if (state->copy_manager == NULL) { + state->capture = false; + g_simple_action_set_state(capture_action, g_variant_new_boolean(state->capture)); + g_simple_action_set_enabled(capture_action, FALSE); + } + + int n_monitors = gdk_display_get_n_monitors(gdk_display); + for (int i = 0; i < n_monitors; i++) { + GdkMonitor *monitor = gdk_display_get_monitor(gdk_display, i); + wd_add_output(state, gdk_wayland_monitor_get_wl_output(monitor)); + } + + g_signal_connect(gdk_display, "monitor-added", G_CALLBACK(monitor_added), state); + g_signal_connect(gdk_display, "monitor-removed", G_CALLBACK(monitor_removed), state); gtk_application_add_window(app, GTK_WINDOW(window)); gtk_widget_show_all(window); diff --git a/src/meson.build b/src/meson.build index a70af58..f2b31a2 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,21 +1,27 @@ cc = meson.get_compiler('c') m_dep = cc.find_library('m', required : false) +rt_dep = cc.find_library('rt', required : false) gdk = dependency('gdk-3.0') gtk = dependency('gtk+-3.0') assert(gdk.get_pkgconfig_variable('targets').split().contains('wayland'), 'Wayland GDK backend not present') +epoxy = dependency('epoxy') executable( 'wdisplay', [ 'main.c', 'outputs.c', + 'render.c', + 'glviewport.c', resources, ], dependencies : [ m_dep, + rt_dep, wayland_client, client_protos, + epoxy, gtk ], install: true diff --git a/src/outputs.c b/src/outputs.c index 363629d..1c32518 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -28,15 +28,26 @@ * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/main.c */ +#define _GNU_SOURCE #include #include #include #include +#include + +#include +#include +#include #include "wdisplay.h" -#include "wlr-output-management-unstable-v1-client-protocol.h" -#define HEADS_MAX 64 +#include "wlr-output-management-unstable-v1-client-protocol.h" +#include "xdg-output-unstable-v1-client-protocol.h" +#include "wlr-screencopy-unstable-v1-client-protocol.h" + +static void noop() { + // This space is intentionally left blank +} struct wd_pending_config { struct wd_state *state; @@ -143,6 +154,171 @@ void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs) { zwlr_output_configuration_v1_apply(config); } +static void wd_frame_destroy(struct wd_frame *frame) { + if (frame->pixels != NULL) + munmap(frame->pixels, frame->height * frame->stride); + if (frame->buffer != NULL) + wl_buffer_destroy(frame->buffer); + if (frame->pool != NULL) + wl_shm_pool_destroy(frame->pool); + if (frame->capture_fd != -1) + close(frame->capture_fd); + if (frame->wlr_frame != NULL) + zwlr_screencopy_frame_v1_destroy(frame->wlr_frame); + + wl_list_remove(&frame->link); + free(frame); +} + +static void capture_buffer(void *data, + struct zwlr_screencopy_frame_v1 *copy_frame, + uint32_t format, uint32_t width, uint32_t height, uint32_t stride) { + struct wd_frame *frame = data; + + char *shm_name = NULL; + if (asprintf(&shm_name, "/wd-%s", frame->output->name) == -1) { + fprintf(stderr, "asprintf: %s\n", strerror(errno)); + shm_name = NULL; + goto err; + } + frame->capture_fd = shm_open(shm_name, O_CREAT | O_RDWR, 0); + if (frame->capture_fd == -1) { + fprintf(stderr, "shm_open: %s\n", strerror(errno)); + goto err; + } + shm_unlink(shm_name); + free(shm_name); + + size_t size = stride * height; + ftruncate(frame->capture_fd, size); + frame->pool = wl_shm_create_pool(frame->output->state->shm, + frame->capture_fd, size); + frame->buffer = wl_shm_pool_create_buffer(frame->pool, 0, + width, height, stride, format); + zwlr_screencopy_frame_v1_copy(copy_frame, frame->buffer); + frame->stride = stride; + frame->width = width; + frame->height = height; + + return; +err: + if (shm_name != NULL) { + free(shm_name); + } + wd_frame_destroy(frame); +} + +static void capture_flags(void *data, + struct zwlr_screencopy_frame_v1 *wlr_frame, + uint32_t flags) { + struct wd_frame *frame = data; + frame->y_invert = !!(flags & ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT); +} + +static void capture_ready(void *data, + struct zwlr_screencopy_frame_v1 *wlr_frame, + uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec) { + struct wd_frame *frame = data; + + frame->pixels = mmap(NULL, frame->stride * frame->height, + PROT_READ, MAP_SHARED, frame->capture_fd, 0); + if (frame->pixels == MAP_FAILED) { + frame->pixels = NULL; + fprintf(stderr, "mmap: %d: %s\n", frame->capture_fd, strerror(errno)); + wd_frame_destroy(frame); + return; + } else { + uint64_t tv_sec = (uint64_t) tv_sec_hi << 32 | tv_sec_lo; + frame->tick = (tv_sec * 1000000) + (tv_nsec / 1000); + } + + zwlr_screencopy_frame_v1_destroy(frame->wlr_frame); + frame->wlr_frame = NULL; + + struct wd_frame *frame_iter, *frame_tmp; + wl_list_for_each_safe(frame_iter, frame_tmp, &frame->output->frames, link) { + if (frame != frame_iter) { + wd_frame_destroy(frame_iter); + } + } +} + +static void capture_failed(void *data, + struct zwlr_screencopy_frame_v1 *wlr_frame) { + struct wd_frame *frame = data; + wd_frame_destroy(frame); +} + +struct zwlr_screencopy_frame_v1_listener capture_listener = { + .buffer = capture_buffer, + .flags = capture_flags, + .ready = capture_ready, + .failed = capture_failed +}; + +static bool has_pending_captures(struct wd_state *state) { + struct wd_output *output; + wl_list_for_each(output, &state->outputs, link) { + struct wd_frame *frame; + wl_list_for_each(frame, &output->frames, link) { + if (frame->pixels == NULL) { + return true; + } + } + } + return false; +} + +void wd_capture_frame(struct wd_state *state) { + if (state->copy_manager == NULL || has_pending_captures(state) + || !state->capture) { + return; + } + + struct wd_output *output; + wl_list_for_each(output, &state->outputs, link) { + struct wd_frame *frame = calloc(1, sizeof(*frame)); + frame->output = output; + frame->capture_fd = -1; + frame->wlr_frame = + zwlr_screencopy_manager_v1_capture_output(state->copy_manager, 1, + output->wl_output); + zwlr_screencopy_frame_v1_add_listener(frame->wlr_frame, &capture_listener, + frame); + wl_list_insert(&output->frames, &frame->link); + } +} + +static void wd_output_destroy(struct wd_output *output) { + struct wd_frame *frame, *frame_tmp; + wl_list_for_each_safe(frame, frame_tmp, &output->frames, link) { + wd_frame_destroy(frame); + } + zxdg_output_v1_destroy(output->xdg_output); + free(output->name); + free(output); +} + +static void wd_mode_destroy(struct wd_mode* mode) { + zwlr_output_mode_v1_destroy(mode->wlr_mode); + free(mode); +} + +static void wd_head_destroy(struct wd_head *head) { + struct wd_mode *mode, *mode_tmp; + if (head->state->clicked == head) { + head->state->clicked = NULL; + } + wl_list_for_each_safe(mode, mode_tmp, &head->modes, link) { + zwlr_output_mode_v1_destroy(mode->wlr_mode); + free(mode); + } + zwlr_output_head_v1_destroy(head->wlr_head); + free(head->name); + free(head->description); + free(head); +} + static void mode_handle_size(void *data, struct zwlr_output_mode_v1 *wlr_mode, int32_t width, int32_t height) { struct wd_mode *mode = data; @@ -166,8 +342,7 @@ static void mode_handle_finished(void *data, struct zwlr_output_mode_v1 *wlr_mode) { struct wd_mode *mode = data; wl_list_remove(&mode->link); - zwlr_output_mode_v1_destroy(mode->wlr_mode); - free(mode); + wd_mode_destroy(mode); } static const struct zwlr_output_mode_v1_listener mode_listener = { @@ -217,6 +392,7 @@ static void head_handle_enabled(void *data, struct wd_head *head = data; head->enabled = !!enabled; if (!enabled) { + head->output = NULL; head->mode = NULL; } wd_ui_reset_head(head, WD_FIELD_ENABLED); @@ -264,10 +440,7 @@ static void head_handle_finished(void *data, struct zwlr_output_head_v1 *wlr_head) { struct wd_head *head = data; wl_list_remove(&head->link); - zwlr_output_head_v1_destroy(head->wlr_head); - free(head->name); - free(head->description); - free(head); + wd_head_destroy(head); } static const struct zwlr_output_head_v1_listener head_listener = { @@ -307,41 +480,169 @@ static void output_manager_handle_done(void *data, wd_ui_reset_heads(state); } -static void output_manager_handle_finished(void *data, - struct zwlr_output_manager_v1 *manager) { - // This space is intentionally left blank -} - static const struct zwlr_output_manager_v1_listener output_manager_listener = { .head = output_manager_handle_head, .done = output_manager_handle_done, - .finished = output_manager_handle_finished, + .finished = noop, }; - static void registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { struct wd_state *state = data; if (strcmp(interface, zwlr_output_manager_v1_interface.name) == 0) { - state->output_manager = wl_registry_bind(registry, name, &zwlr_output_manager_v1_interface, 1); - zwlr_output_manager_v1_add_listener(state->output_manager, &output_manager_listener, state); + state->output_manager = wl_registry_bind(registry, name, + &zwlr_output_manager_v1_interface, version); + zwlr_output_manager_v1_add_listener(state->output_manager, + &output_manager_listener, state); + } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { + state->xdg_output_manager = wl_registry_bind(registry, name, + &zxdg_output_manager_v1_interface, version); + } else if(strcmp(interface, zwlr_screencopy_manager_v1_interface.name) == 0) { + state->copy_manager = wl_registry_bind(registry, name, + &zwlr_screencopy_manager_v1_interface, version); + } else if(strcmp(interface, wl_shm_interface.name) == 0) { + state->shm = wl_registry_bind(registry, name, &wl_shm_interface, version); } } -static void registry_handle_global_remove(void *data, - struct wl_registry *registry, uint32_t name) { - // This space is intentionally left blank -} - static const struct wl_registry_listener registry_listener = { .global = registry_handle_global, - .global_remove = registry_handle_global_remove, + .global_remove = noop, }; -void wd_add_output_management_listener(struct wd_state *state, struct wl_display *display) { +void wd_add_output_management_listener(struct wd_state *state, struct + wl_display *display) { struct wl_registry *registry = wl_display_get_registry(display); wl_registry_add_listener(registry, ®istry_listener, state); wl_display_dispatch(display); wl_display_roundtrip(display); } + +static struct wd_head *wd_find_head(struct wd_state *state, + struct wd_output *output) { + struct wd_head *head; + wl_list_for_each(head, &state->heads, link) { + if (output->name != NULL && strcmp(output->name, head->name) == 0) { + return head; + } + } + return NULL; +} + +static void output_logical_position(void *data, struct zxdg_output_v1 *zxdg_output_v1, + int32_t x, int32_t y) { + struct wd_output *output = data; + struct wd_head *head = wd_find_head(output->state, output); + if (head != NULL) { + head->x = x; + head->y = y; + wd_ui_reset_head(head, WD_FIELD_POSITION); + } +} + +static void output_logical_size(void *data, struct zxdg_output_v1 *zxdg_output_v1, + int32_t width, int32_t height) { + struct wd_output *output = data; + struct wd_head *head = wd_find_head(output->state, output); + if (head != NULL) { + struct wd_mode *mode; + head->custom_mode.width = width; + head->custom_mode.height = height; + head->mode = NULL; + wl_list_for_each(mode, &head->modes, link) { + if (mode->width == width && mode->height == height) { + head->mode = mode; + return; + } + } + wd_ui_reset_head(head, WD_FIELD_MODE); + } +} + +static void output_name(void *data, struct zxdg_output_v1 *zxdg_output_v1, + const char *name) { + struct wd_output *output = data; + output->name = strdup(name); +} + +static const struct zxdg_output_v1_listener output_listener = { + .logical_position = output_logical_position, + .logical_size = output_logical_size, + .done = noop, + .name = output_name, + .description = noop +}; + +void wd_add_output(struct wd_state *state, struct wl_output *wl_output) { + struct wd_output *output = calloc(1, sizeof(*output)); + output->state = state; + output->wl_output = wl_output; + output->xdg_output = zxdg_output_manager_v1_get_xdg_output( + state->xdg_output_manager, wl_output); + wl_list_init(&output->frames); + zxdg_output_v1_add_listener(output->xdg_output, &output_listener, output); + wl_list_insert(&output->state->outputs, &output->link); +} + +void wd_remove_output(struct wd_state *state, struct wl_output *wl_output, + struct wl_display *display) { + struct wd_output *output, *output_tmp; + wl_list_for_each_safe(output, output_tmp, &state->outputs, link) { + if (output->wl_output == wl_output) { + wl_list_remove(&output->link); + wd_output_destroy(output); + break; + } + } + wd_capture_wait(state, display); +} + +struct wd_output *wd_find_output(struct wd_state *state, struct wd_head + *head) { + if (!head->enabled) { + return NULL; + } + if (head->output != NULL) { + return head->output; + } + struct wd_output *output; + wl_list_for_each(output, &state->outputs, link) { + if (output->name != NULL && strcmp(output->name, head->name) == 0) { + head->output = output; + return output; + } + } + head->output = NULL; + return NULL; +} + +struct wd_state *wd_state_create(void) { + struct wd_state *state = calloc(1, sizeof(*state)); + state->zoom = 1.; + state->capture = true; + wl_list_init(&state->heads); + wl_list_init(&state->outputs); + return state; +} + +void wd_capture_wait(struct wd_state *state, struct wl_display *display) { + wl_display_flush(display); + while (has_pending_captures(state)) { + if (wl_display_dispatch(display) == -1) { + break; + } + } +} + +void wd_state_destroy(struct wd_state *state) { + struct wd_head *head, *head_tmp; + wl_list_for_each_safe(head, head_tmp, &state->heads, link) { + wd_head_destroy(head); + } + struct wd_output *output, *output_tmp; + wl_list_for_each_safe(output, output_tmp, &state->outputs, link) { + wd_output_destroy(output); + } + free(state); +} diff --git a/src/render.c b/src/render.c new file mode 100644 index 0000000..7e64db7 --- /dev/null +++ b/src/render.c @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2019 cyclopsian + + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "wdisplay.h" + +#include +#include +#include +#include + +#define CANVAS_MARGIN 100 + +#define BT_UV_VERT_SIZE (2 + 2) +#define BT_UV_QUAD_SIZE (6 * BT_UV_VERT_SIZE) +#define BT_UV_MAX (BT_COLOR_QUAD_SIZE * HEADS_MAX) + +#define BT_COLOR_VERT_SIZE (2 + 4) +#define BT_COLOR_QUAD_SIZE (6 * BT_COLOR_VERT_SIZE) +#define BT_COLOR_MAX (BT_COLOR_QUAD_SIZE * HEADS_MAX) + +struct wd_gl_data { + GLuint color_program; + GLuint color_vertex_shader; + GLuint color_fragment_shader; + GLuint color_position_attribute; + GLuint color_color_attribute; + GLuint color_screen_size_uniform; + + GLuint texture_program; + GLuint texture_vertex_shader; + GLuint texture_fragment_shader; + GLuint texture_position_attribute; + GLuint texture_uv_attribute; + GLuint texture_screen_size_uniform; + GLuint texture_texture_uniform; + + GLuint buffers[2]; + + unsigned texture_count; + GLuint textures[HEADS_MAX]; + + float tris[BT_COLOR_MAX]; +}; + +static const char *color_vertex_shader_src = "\ +attribute vec2 position;\n\ +attribute vec4 color;\n\ +varying vec4 color_out;\n\ +uniform vec2 screen_size;\n\ +void main(void) {\n\ + vec2 screen_pos = (position / screen_size * 2. - 1.) * vec2(1., -1.);\n\ + gl_Position = vec4(screen_pos, 0., 1.);\n\ + color_out = color;\n\ +}"; + +static const char *color_fragment_shader_src = "\ +varying vec4 color_out;\n\ +void main(void) {\n\ + gl_FragColor = color_out;\n\ +}"; + +static const char *texture_vertex_shader_src = "\ +attribute vec2 position;\n\ +attribute vec2 uv;\n\ +varying vec2 uv_out;\n\ +uniform vec2 screen_size;\n\ +void main(void) {\n\ + vec2 screen_pos = (position / screen_size * 2. - 1.) * vec2(1., -1.);\n\ + gl_Position = vec4(screen_pos, 0., 1.);\n\ + uv_out = uv;\n\ +}"; + +static const char *texture_fragment_shader_src = "\ +varying vec2 uv_out;\n\ +uniform sampler2D texture;\n\ +void main(void) {\n\ + gl_FragColor = texture2D(texture, uv_out);\n\ +}"; + +static GLuint gl_make_shader(GLenum type, const char *src) { + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &src, NULL); + glCompileShader(shader); + GLint status; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if (status == GL_FALSE) { + GLsizei length; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); + GLchar *log = "Failed"; + if (length > 0) { + log = malloc(length); + glGetShaderInfoLog(shader, length, NULL, log); + } + fprintf(stderr, "glCompileShader: %s\n", log); + if (length > 0) { + free(log); + } + } + return shader; +} + +static void gl_link_and_validate(GLint program) { + GLint status; + + glLinkProgram(program); + glGetProgramiv(program, GL_LINK_STATUS, &status); + if (status == GL_FALSE) { + GLsizei length; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); + GLchar *log = malloc(length); + glGetProgramInfoLog(program, length, NULL, log); + fprintf(stderr, "glLinkProgram: %s\n", log); + free(log); + return; + } + glValidateProgram(program); + glGetProgramiv(program, GL_VALIDATE_STATUS, &status); + if (status == GL_FALSE) { + GLsizei length; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); + GLchar *log = malloc(length); + glGetProgramInfoLog(program, length, NULL, log); + fprintf(stderr, "glValidateProgram: %s\n", log); + free(log); + } +} + +struct wd_gl_data *wd_gl_setup(void) { + struct wd_gl_data *res = calloc(1, sizeof(struct wd_gl_data)); + res->color_program = glCreateProgram(); + + res->color_vertex_shader = gl_make_shader(GL_VERTEX_SHADER, + color_vertex_shader_src); + glAttachShader(res->color_program, res->color_vertex_shader); + res->color_fragment_shader = gl_make_shader(GL_FRAGMENT_SHADER, + color_fragment_shader_src); + glAttachShader(res->color_program, res->color_fragment_shader); + gl_link_and_validate(res->color_program); + + res->color_position_attribute = glGetAttribLocation(res->color_program, + "position"); + res->color_color_attribute = glGetAttribLocation(res->color_program, + "color"); + res->color_screen_size_uniform = glGetUniformLocation(res->color_program, + "screen_size"); + + res->texture_program = glCreateProgram(); + + res->texture_vertex_shader = gl_make_shader(GL_VERTEX_SHADER, + texture_vertex_shader_src); + glAttachShader(res->texture_program, res->texture_vertex_shader); + res->texture_fragment_shader = gl_make_shader(GL_FRAGMENT_SHADER, + texture_fragment_shader_src); + glAttachShader(res->texture_program, res->texture_fragment_shader); + gl_link_and_validate(res->texture_program); + + res->texture_position_attribute = glGetAttribLocation(res->texture_program, + "position"); + res->texture_uv_attribute = glGetAttribLocation(res->texture_program, + "uv"); + res->texture_screen_size_uniform = glGetUniformLocation(res->texture_program, + "screen_size"); + res->texture_texture_uniform = glGetUniformLocation(res->texture_program, + "texture"); + + glGenBuffers(2, res->buffers); + glBindBuffer(GL_ARRAY_BUFFER, res->buffers[0]); + glBufferData(GL_ARRAY_BUFFER, BT_UV_MAX * sizeof(float), + NULL, GL_DYNAMIC_DRAW); + + glBindBuffer(GL_ARRAY_BUFFER, res->buffers[1]); + glBufferData(GL_ARRAY_BUFFER, BT_COLOR_MAX * sizeof(float), + NULL, GL_DYNAMIC_DRAW); + + return res; +} + +#define PUSH_POINT(_start, _a, _b) \ + *((_start)++) = (_a);\ + *((_start)++) = (_b); + +#define PUSH_COLOR(_start, _a, _b, _c, _d) \ + *((_start)++) = (_a);\ + *((_start)++) = (_b);\ + *((_start)++) = (_c);\ + *((_start)++) = (_d); + +#define PUSH_POINT_UV(_start, _a, _b, _c, _d) \ + PUSH_COLOR(_start, _a, _b, _c, _d) + +void wd_gl_render(struct wd_gl_data *res, struct wd_render_data *info, + uint64_t tick) { + unsigned int tris = 0; + + if (info->head_count > res->texture_count) { + glGenTextures(info->head_count - res->texture_count, + res->textures + res->texture_count); + for (int i = res->texture_count; i < info->head_count; i++) { + glBindTexture(GL_TEXTURE_2D, res->textures[i]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + glBindTexture(GL_TEXTURE_2D, 0); + res->texture_count = info->head_count; + } + + for (int i = 0; i < info->head_count; i++) { + struct wd_render_head_data *head = &info->heads[i]; + float *tri_ptr = res->tris + i * BT_UV_QUAD_SIZE; + float x1 = head->x1; + float y1 = head->y1; + float x2 = head->x2; + float y2 = head->y2; + + float t1 = head->y_invert ? 1.f : 0.f; + float t2 = head->y_invert ? 0.f : 1.f; + + PUSH_POINT_UV(tri_ptr, x1, y1, 0.f, t1) + PUSH_POINT_UV(tri_ptr, x2, y1, 1.f, t1) + PUSH_POINT_UV(tri_ptr, x1, y2, 0.f, t2) + PUSH_POINT_UV(tri_ptr, x1, y2, 0.f, t2) + PUSH_POINT_UV(tri_ptr, x2, y1, 1.f, t1) + PUSH_POINT_UV(tri_ptr, x2, y2, 1.f, t2) + + tris += 6; + } + + glClearColor(info->bg_color[0], info->bg_color[1], info->bg_color[2], 1.f); + glClear(GL_COLOR_BUFFER_BIT); + + float screen_size[2] = { info->viewport_width, info->viewport_height }; + + if (tris > 0) { + glUseProgram(res->texture_program); + glBindBuffer(GL_ARRAY_BUFFER, res->buffers[0]); + glBufferSubData(GL_ARRAY_BUFFER, 0, + tris * BT_UV_VERT_SIZE * sizeof(float), res->tris); + glEnableVertexAttribArray(res->texture_position_attribute); + glEnableVertexAttribArray(res->texture_uv_attribute); + glVertexAttribPointer(res->texture_position_attribute, + 2, GL_FLOAT, GL_FALSE, + BT_UV_VERT_SIZE * sizeof(float), (void *) (0 * sizeof(float))); + glVertexAttribPointer(res->texture_uv_attribute, 2, GL_FLOAT, GL_FALSE, + BT_UV_VERT_SIZE * sizeof(float), (void *) (2 * sizeof(float))); + glUniform2fv(res->texture_screen_size_uniform, 1, screen_size); + glUniform1i(res->texture_texture_uniform, 0); + glActiveTexture(GL_TEXTURE0); + + for (int i = 0; i < info->head_count; i++) { + struct wd_render_head_data *head = &info->heads[i]; + glBindTexture(GL_TEXTURE_2D, res->textures[i]); + if (head->updated_at == tick) { + glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, head->tex_stride / 4); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, + head->tex_width, head->tex_height, + 0, GL_RGBA, GL_UNSIGNED_BYTE, head->pixels); + glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, 0); + } + glDrawArrays(GL_TRIANGLES, i * 6, 6); + } + } + + tris = 0; + + int j = 0; + for (int i = 0; i < info->head_count; i++) { + struct wd_render_head_data *head = &info->heads[i]; + if (head->hovered || tick < head->transition_begin + HOVER_USECS) { + float *tri_ptr = res->tris + j++ * BT_COLOR_QUAD_SIZE; + float x1 = head->x1; + float y1 = head->y1; + float x2 = head->x2; + float y2 = head->y2; + + float *color = info->selection_color; + float d = fminf( + (tick - head->transition_begin) / (double) HOVER_USECS, 1.f); + if (!head->hovered) { + d = 1.f - d; + } + d *= 2.f; + if (d <= 1.f) { + d = d * d; + } else { + d -= 1.f; + d = d * (2.f - d) + 1.f; + } + d /= 2.f; + float alpha = color[3] * d * .5f; + + PUSH_POINT(tri_ptr, x1, y1) + PUSH_COLOR(tri_ptr, color[0], color[1], color[2], alpha) + PUSH_POINT(tri_ptr, x2, y1) + PUSH_COLOR(tri_ptr, color[0], color[1], color[2], alpha) + PUSH_POINT(tri_ptr, x1, y2) + PUSH_COLOR(tri_ptr, color[0], color[1], color[2], alpha) + PUSH_POINT(tri_ptr, x1, y2) + PUSH_COLOR(tri_ptr, color[0], color[1], color[2], alpha) + PUSH_POINT(tri_ptr, x2, y1) + PUSH_COLOR(tri_ptr, color[0], color[1], color[2], alpha) + PUSH_POINT(tri_ptr, x2, y2) + PUSH_COLOR(tri_ptr, color[0], color[1], color[2], alpha) + + tris += 6; + } + } + + if (tris > 0) { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glUseProgram(res->color_program); + glBindBuffer(GL_ARRAY_BUFFER, res->buffers[1]); + glBufferSubData(GL_ARRAY_BUFFER, 0, + tris * BT_COLOR_VERT_SIZE * sizeof(float), res->tris); + glEnableVertexAttribArray(res->color_position_attribute); + glEnableVertexAttribArray(res->color_color_attribute); + glVertexAttribPointer(res->color_position_attribute, 2, GL_FLOAT, GL_FALSE, + BT_COLOR_VERT_SIZE * sizeof(float), (void *) (0 * sizeof(float))); + glVertexAttribPointer(res->color_color_attribute, 4, GL_FLOAT, GL_FALSE, + BT_COLOR_VERT_SIZE * sizeof(float), (void *) (2 * sizeof(float))); + glUniform2fv(res->color_screen_size_uniform, 1, screen_size); + glDrawArrays(GL_TRIANGLES, 0, tris); + glDisable(GL_BLEND); + } +} + +void wd_gl_cleanup(struct wd_gl_data *res) { + glDeleteBuffers(2, res->buffers); + glDeleteShader(res->texture_fragment_shader); + glDeleteShader(res->texture_vertex_shader); + glDeleteProgram(res->texture_program); + + glDeleteShader(res->color_fragment_shader); + glDeleteShader(res->color_vertex_shader); + glDeleteProgram(res->color_program); + + free(res); +} diff --git a/src/wdisplay.h b/src/wdisplay.h index a7ce955..3131a64 100644 --- a/src/wdisplay.h +++ b/src/wdisplay.h @@ -31,16 +31,28 @@ #ifndef WDISPLAY_WDISPLAY_H #define WDISPLAY_WDISPLAY_H +#define HEADS_MAX 64 +#define HOVER_USECS (100 * 1000) + #include #include +struct zxdg_output_v1; +struct zxdg_output_manager_v1; struct zwlr_output_mode_v1; struct zwlr_output_head_v1; struct zwlr_output_manager_v1; +struct zwlr_screencopy_manager_v1; +struct zwlr_screencopy_frame_v1; + struct _GtkWidget; typedef struct _GtkWidget GtkWidget; struct _GtkBuilder; typedef struct _GtkBuilder GtkBuilder; +struct _GdkCursor; +typedef struct _GdkCursor GdkCursor; +struct _cairo_surface; +typedef struct _cairo_surface cairo_surface_t; enum wd_head_fields { WD_FIELD_NAME = 1 << 0, @@ -54,6 +66,32 @@ enum wd_head_fields { WD_FIELDS_ALL = (1 << 8) - 1 }; +struct wd_output { + struct wd_state *state; + struct zxdg_output_v1 *xdg_output; + struct wl_output *wl_output; + struct wl_list link; + + char *name; + struct wl_list frames; +}; + +struct wd_frame { + struct wd_output *output; + struct zwlr_screencopy_frame_v1 *wlr_frame; + + struct wl_list link; + int capture_fd; + unsigned stride; + unsigned width; + unsigned height; + struct wl_shm_pool *pool; + struct wl_buffer *buffer; + uint8_t *pixels; + uint64_t tick; + bool y_invert; +}; + struct wd_head_config { struct wl_list link; @@ -83,6 +121,10 @@ struct wd_head { struct zwlr_output_head_v1 *wlr_head; struct wl_list link; + struct wd_output *output; + struct wd_render_head_data *render; + cairo_surface_t *surface; + char *name, *description; int32_t phys_width, phys_height; // mm struct wl_list modes; @@ -98,16 +140,69 @@ struct wd_head { double scale; }; +struct wd_gl_data; + +struct wd_render_head_data { + float x1; + float y1; + float x2; + float y2; + + uint8_t *pixels; + unsigned tex_stride; + unsigned tex_width; + unsigned tex_height; + bool preview; + bool y_invert; + uint64_t updated_at; + + bool hovered; + uint64_t transition_begin; +}; + +struct wd_render_data { + float fg_color[4]; + float bg_color[4]; + float border_color[4]; + float selection_color[4]; + unsigned int viewport_width; + unsigned int viewport_height; + unsigned int width; + unsigned int height; + int scroll_x; + int scroll_y; + int x_origin; + int y_origin; + uint64_t updated_at; + + unsigned int head_count; + struct wd_render_head_data heads[HEADS_MAX]; +}; + +struct wd_point { + double x; + double y; +}; + struct wd_state { + struct zxdg_output_manager_v1 *xdg_output_manager; struct zwlr_output_manager_v1 *output_manager; + struct zwlr_screencopy_manager_v1 *copy_manager; + struct wl_shm *shm; struct wl_list heads; + struct wl_list outputs; uint32_t serial; bool apply_pending; bool autoapply; + bool capture; double zoom; - int xorigin; - int yorigin; + + struct wd_head *clicked; + /* top left, bottom right */ + struct wd_point click_offset; + bool panning; + struct wd_point pan_last; GtkWidget *header_stack; GtkWidget *stack_switcher; @@ -122,13 +217,47 @@ struct wd_state { GtkWidget *info_bar; GtkWidget *info_label; GtkWidget *menu_button; + + GdkCursor *grab_cursor; + GdkCursor *grabbing_cursor; + GdkCursor *move_cursor; + + unsigned int canvas_tick; + struct wd_gl_data *gl_data; + struct wd_render_data render; }; + +/* + * Creates the application state structure. + */ +struct wd_state *wd_state_create(void); + +/* + * Frees the application state structure. + */ +void wd_state_destroy(struct wd_state *state); + /* * Displays an error message and then exits the program. */ void wd_fatal_error(int status, const char *message); +/* + * Add an output to the list of screen captured outputs. + */ +void wd_add_output(struct wd_state *state, struct wl_output *wl_output); + +/* + * Remove an output from the list of screen captured outputs. + */ +void wd_remove_output(struct wd_state *state, struct wl_output *wl_output, struct wl_display *display); + +/* + * Finds the output associated with a given head. Can return NULL if the head's + * output is disabled. + */ +struct wd_output *wd_find_output(struct wd_state *state, struct wd_head *head); /* * Starts listening for output management events from the compositor. */ @@ -139,6 +268,16 @@ void wd_add_output_management_listener(struct wd_state *state, struct wl_display */ void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs); +/* + * Queues capture of the next frame of all screens. + */ +void wd_capture_frame(struct wd_state *state); + +/* + * Blocks until all captures are finished. + */ +void wd_capture_wait(struct wd_state *state, struct wl_display *display); + /* * Updates the UI stack of all heads. Does not update individual head forms. * Useful for when a display is plugged/unplugged and we want to add/remove @@ -147,7 +286,8 @@ void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs); void wd_ui_reset_heads(struct wd_state *state); /* - * Updates a form with head configuration from the server. Only updates specified fields. + * Updates the UI form for a single head. Useful for when the compositor + * notifies us of updated configuration caused by another program. */ void wd_ui_reset_head(const struct wd_head *head, unsigned int fields); @@ -166,4 +306,17 @@ void wd_ui_apply_done(struct wd_state *state, struct wl_list *outputs); */ void wd_ui_show_error(struct wd_state *state, const char *message); +/* + * Compiles the GL shaders. + */ +struct wd_gl_data *wd_gl_setup(void); +/* + * Renders the GL scene. + */ +void wd_gl_render(struct wd_gl_data *res, struct wd_render_data *info, uint64_t tick); +/* + * Destroys the GL shaders. + */ +void wd_gl_cleanup(struct wd_gl_data *res); + #endif