feat(paint): introduce text paint

This commit is contained in:
Jeremy Attali 2020-01-01 16:37:08 -05:00
parent 87fd4db304
commit 3347bf23bf
9 changed files with 292 additions and 10 deletions

View File

@ -95,6 +95,7 @@ Install dependencies:
- meson
- wayland
- cairo
- pango
- gtk
Optional dependencies:

View File

@ -1,10 +1,17 @@
#pragma once
#include <gdk/gdk.h>
#include "swappy.h"
void paint_add_temporary(struct swappy_state *state, double x, double y,
enum swappy_paint_type type);
void paint_update_temporary(struct swappy_state *state, double x, double y);
void paint_update_temporary_shape(struct swappy_state *state, double x,
double y);
void paint_update_temporary_text(struct swappy_state *state,
GdkEventKey *event);
void paint_update_temporary_text_clip(struct swappy_state *state, gdouble x,
gdouble y);
void paint_commit_temporary(struct swappy_state *state);
void paint_free(gpointer data);

View File

@ -18,6 +18,8 @@
#define SWAPPY_STROKE_SIZE_MIN 1
#define SWAPPY_STROKE_SIZE_DEFAULT 5
#define SWAPPY_TEXT_FONT_DEFAULT "serif"
#define SWAPPY_TEXT_SIZE_DEFAULT 21
#define SWAPPY_STROKE_SIZE_MAX 50
enum swappy_paint_type {
@ -28,11 +30,29 @@ enum swappy_paint_type {
SWAPPY_PAINT_MODE_ARROW, /* Arrow shapes */
};
enum swappy_text_mode {
SWAPPY_TEXT_MODE_EDIT = 0,
SWAPPY_TEXT_MODE_DONE,
};
struct swappy_point {
gdouble x;
gdouble y;
};
struct swappy_paint_text {
double r;
double g;
double b;
double a;
double s;
gchar *text;
int cursor;
struct swappy_point from;
struct swappy_point to;
enum swappy_text_mode mode;
};
struct swappy_paint_shape {
double r;
double g;
@ -59,6 +79,7 @@ struct swappy_paint {
union {
struct swappy_paint_brush brush;
struct swappy_paint_shape shape;
struct swappy_paint_text text;
} content;
};
@ -75,6 +96,7 @@ struct swappy_state_settings {
double b;
double a;
double w;
double t;
};
struct swappy_state_ui {

7
include/util.h Normal file
View File

@ -0,0 +1,7 @@
#pragma once
#include <glib.h>
void string_remove_at(char *str, int pos);
gchar *string_insert_char_at(gchar *str, gchar c, int pos);

View File

@ -23,6 +23,7 @@ if cc.get_id() == 'clang'
endif
cairo = dependency('cairo')
pango = dependency('pango')
math = cc.find_library('m')
realtime = cc.find_library('rt')
gtk = dependency('gtk+-3.0', version: '>=3.20.0')
@ -58,10 +59,12 @@ executable(
'src/paint.c',
'src/render.c',
'src/notification.c',
'src/util.c',
'src/wayland.c',
]),
dependencies: [
cairo,
pango,
client_protos,
gtk,
libnotify,

View File

@ -212,6 +212,11 @@ void copy_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
void window_keypress_handler(GtkWidget *widget, GdkEventKey *event,
struct swappy_state *state) {
if (state->temp_paint && state->mode == SWAPPY_PAINT_MODE_TEXT) {
paint_update_temporary_text(state, event);
render_state(state);
return;
}
if (event->state & GDK_CONTROL_MASK) {
switch (event->keyval) {
case GDK_KEY_c:
@ -336,8 +341,10 @@ void draw_area_button_press_handler(GtkWidget *widget, GdkEventButton *event,
case SWAPPY_PAINT_MODE_RECTANGLE:
case SWAPPY_PAINT_MODE_ELLIPSE:
case SWAPPY_PAINT_MODE_ARROW:
case SWAPPY_PAINT_MODE_TEXT:
paint_add_temporary(state, event->x, event->y, state->mode);
render_state(state);
update_ui_undo_redo(state);
break;
default:
return;
@ -346,6 +353,8 @@ void draw_area_button_press_handler(GtkWidget *widget, GdkEventButton *event,
}
void draw_area_motion_notify_handler(GtkWidget *widget, GdkEventMotion *event,
struct swappy_state *state) {
gdouble x = event->x;
gdouble y = event->y;
GdkDisplay *display = gdk_display_get_default();
GdkWindow *window = event->window;
GdkCursor *crosshair = gdk_cursor_new_for_display(display, GDK_CROSSHAIR);
@ -359,7 +368,13 @@ void draw_area_motion_notify_handler(GtkWidget *widget, GdkEventMotion *event,
case SWAPPY_PAINT_MODE_ELLIPSE:
case SWAPPY_PAINT_MODE_ARROW:
if (is_button1_pressed) {
paint_update_temporary(state, event->x, event->y);
paint_update_temporary_shape(state, x, y);
render_state(state);
}
break;
case SWAPPY_PAINT_MODE_TEXT:
if (is_button1_pressed) {
paint_update_temporary_text_clip(state, x, y);
render_state(state);
}
break;
@ -384,6 +399,12 @@ void draw_area_button_release_handler(GtkWidget *widget, GdkEventButton *event,
render_state(state);
update_ui_undo_redo(state);
break;
case SWAPPY_PAINT_MODE_TEXT:
if (state->temp_paint && !state->temp_paint->can_draw) {
paint_free(state->temp_paint);
state->temp_paint = NULL;
}
break;
default:
return;
}
@ -620,6 +641,7 @@ bool application_init(struct swappy_state *state) {
state->settings.b = 0;
state->settings.a = 1;
state->settings.w = SWAPPY_STROKE_SIZE_DEFAULT;
state->settings.t = SWAPPY_TEXT_SIZE_DEFAULT;
return true;
}

View File

@ -1,5 +1,7 @@
#include "paint.h"
#include "util.h"
void paint_free(gpointer data) {
struct swappy_paint *paint = (struct swappy_paint *)data;
@ -10,11 +12,14 @@ void paint_free(gpointer data) {
switch (paint->type) {
case SWAPPY_PAINT_MODE_BRUSH:
g_list_free_full(paint->content.brush.points, g_free);
g_free(paint);
break;
case SWAPPY_PAINT_MODE_TEXT:
g_free(paint->content.text.text);
break;
default:
g_free(paint);
break;
}
g_free(paint);
}
void paint_free_list(GList **list) {
@ -41,9 +46,19 @@ void paint_add_temporary(struct swappy_state *state, double x, double y,
double b = state->settings.b;
double a = state->settings.a;
double w = state->settings.w;
double t = state->settings.t;
paint->type = type;
if (state->temp_paint) {
if (type == SWAPPY_PAINT_MODE_TEXT) {
paint_commit_temporary(state);
} else {
g_free(state->temp_paint);
state->temp_paint = NULL;
}
}
switch (type) {
case SWAPPY_PAINT_MODE_BRUSH:
paint->can_draw = true;
@ -74,19 +89,32 @@ void paint_add_temporary(struct swappy_state *state, double x, double y,
paint->content.shape.w = w;
paint->content.shape.type = type;
break;
case SWAPPY_PAINT_MODE_TEXT:
paint->can_draw = false;
paint->content.text.from.x = x;
paint->content.text.from.y = y;
paint->content.text.r = r;
paint->content.text.g = g;
paint->content.text.b = b;
paint->content.text.a = a;
paint->content.text.s = t;
paint->content.text.cursor = 0;
paint->content.text.mode = SWAPPY_TEXT_MODE_EDIT;
paint->content.text.text = g_new(gchar, 1);
paint->content.text.text[0] = '\0';
break;
default:
g_info("unable to add temporary paint: %d", type);
break;
}
if (state->temp_paint) {
g_free(state->temp_paint);
}
state->temp_paint = paint;
}
void paint_update_temporary(struct swappy_state *state, double x, double y) {
void paint_update_temporary_shape(struct swappy_state *state, double x,
double y) {
struct swappy_paint *paint = state->temp_paint;
struct swappy_point *brush;
GList *points;
@ -113,11 +141,86 @@ void paint_update_temporary(struct swappy_state *state, double x, double y) {
paint->content.shape.to.y = y;
break;
default:
g_info("unable to update temporary paint: %d", paint->type);
g_info("unable to update temporary paint when type is: %d", paint->type);
break;
}
}
void paint_update_temporary_text(struct swappy_state *state,
GdkEventKey *event) {
struct swappy_paint *paint = state->temp_paint;
struct swappy_paint_text *text;
char buffer[32];
guint32 unicode;
if (!paint || paint->type != SWAPPY_PAINT_MODE_TEXT) {
g_warning("trying to update text but not in text mode");
return;
}
text = &paint->content.text;
switch (event->keyval) {
case GDK_KEY_Escape:
paint_commit_temporary(state);
break;
case GDK_KEY_BackSpace:
if (strlen(text->text) > 0) {
string_remove_at(text->text, text->cursor - 1);
text->cursor--;
}
break;
case GDK_KEY_Delete:
if (strlen(text->text) > 0) {
string_remove_at(text->text, text->cursor);
}
break;
case GDK_KEY_Left:
text->cursor--;
break;
case GDK_KEY_Right:
text->cursor++;
break;
default:
unicode = gdk_keyval_to_unicode(event->keyval);
if (unicode != 0) {
int ll = g_unichar_to_utf8(unicode, buffer);
buffer[ll] = '\0';
g_debug("received unicode: %d - utf8: %s (%d)", unicode, buffer, ll);
g_debug("text before: %s - cursor: %d", text->text, text->cursor);
char *new_text =
string_insert_char_at(text->text, buffer[0], text->cursor);
g_free(text->text);
text->text = new_text;
text->cursor++;
}
break;
}
if (text->cursor < 0) {
text->cursor = 0;
} else if (text->cursor > (int)strlen(text->text)) {
text->cursor = (int)strlen(text->text);
}
g_debug("text is now: %s", text->text);
}
void paint_update_temporary_text_clip(struct swappy_state *state, gdouble x,
gdouble y) {
struct swappy_paint *paint = state->temp_paint;
if (!paint) {
return;
}
g_assert(paint->type == SWAPPY_PAINT_MODE_TEXT);
paint->can_draw = true;
paint->content.text.to.x = x;
paint->content.text.to.y = y;
}
void paint_commit_temporary(struct swappy_state *state) {
struct swappy_paint *paint = state->temp_paint;
@ -125,6 +228,18 @@ void paint_commit_temporary(struct swappy_state *state) {
return;
}
switch (paint->type) {
case SWAPPY_PAINT_MODE_TEXT:
if (strlen(paint->content.text.text) == 0) {
paint->can_draw = false;
}
paint->content.text.mode = SWAPPY_TEXT_MODE_DONE;
break;
default:
g_info("unable to update temporary text when type is: %d", paint->type);
break;
}
if (!paint->can_draw) {
paint_free(paint);
} else {

View File

@ -1,12 +1,82 @@
#include <gdk/gdk.h>
#include <gtk/gtk.h>
#include <pango/pangocairo.h>
#include "swappy.h"
#include "util.h"
#ifndef M_PI
#define M_PI (3.14159265358979323846)
#endif
#define RENDER_PANGO_FONT SWAPPY_TEXT_FONT_DEFAULT SWAPPY_TEXT_SIZE_DEFAULT
#define pango_layout_t PangoLayout
#define pango_font_description_t PangoFontDescription
#define pango_rectangle_t PangoRectangle
static void convert_pango_rectangle_to_swappy_box(pango_rectangle_t rectangle,
struct swappy_box *box) {
if (!box) {
return;
}
box->x = (double)rectangle.x / PANGO_SCALE;
box->y = (double)rectangle.y / PANGO_SCALE;
box->width = (double)rectangle.width / PANGO_SCALE;
box->height = (double)rectangle.height / PANGO_SCALE;
}
static void render_text(cairo_t *cr, struct swappy_paint_text text) {
char pango_font[255];
double x = fmin(text.from.x, text.to.x);
double y = fmin(text.from.y, text.to.y);
double w = fabs(text.from.x - text.to.x);
double h = fabs(text.from.y - text.to.y);
cairo_surface_t *surface =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
cairo_t *crt = cairo_create(surface);
pango_layout_t *layout = pango_cairo_create_layout(crt);
pango_layout_set_text(layout, text.text, -1);
snprintf(pango_font, 255, "%s %d", SWAPPY_TEXT_FONT_DEFAULT, (int)text.s);
pango_font_description_t *desc =
pango_font_description_from_string(pango_font);
pango_layout_set_width(layout, pango_units_from_double(w));
pango_layout_set_font_description(layout, desc);
pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
pango_font_description_free(desc);
if (text.mode == SWAPPY_TEXT_MODE_EDIT) {
pango_rectangle_t strong_pos;
pango_rectangle_t weak_pos;
struct swappy_box cursor_box;
cairo_set_source_rgba(cr, 0.5, 0.5, 0.5, 0.5);
cairo_set_line_width(cr, 5);
cairo_rectangle(cr, x, y, w, h);
cairo_stroke(cr);
pango_layout_get_cursor_pos(layout, text.cursor, &strong_pos, &weak_pos);
convert_pango_rectangle_to_swappy_box(strong_pos, &cursor_box);
cairo_move_to(crt, cursor_box.x, cursor_box.y);
cairo_set_source_rgba(crt, 0.3, 0.3, 0.3, 1);
cairo_line_to(crt, cursor_box.x, cursor_box.y + cursor_box.height);
cairo_stroke(crt);
}
cairo_rectangle(crt, 0, 0, w, h);
cairo_set_source_rgba(crt, text.r, text.g, text.b, text.a);
cairo_move_to(crt, 0, 0);
pango_cairo_show_layout(crt, layout);
cairo_set_source_surface(cr, surface, x, y);
cairo_paint(cr);
cairo_destroy(crt);
cairo_surface_destroy(surface);
g_object_unref(layout);
}
static void render_shape_arrow(cairo_t *cr, struct swappy_paint_shape shape) {
cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a);
cairo_set_line_width(cr, shape.w);
@ -171,6 +241,9 @@ static void render_paint(cairo_t *cr, struct swappy_paint *paint) {
case SWAPPY_PAINT_MODE_ARROW:
render_shape(cr, paint->content.shape);
break;
case SWAPPY_PAINT_MODE_TEXT:
render_text(cr, paint->content.text);
break;
default:
g_info("unable to draw paint with type: %d", paint->type);
break;

32
src/util.c Normal file
View File

@ -0,0 +1,32 @@
#include "util.h"
#include <glib.h>
#include <string.h>
void string_remove_at(gchar *str, int pos) {
memmove(&str[pos], &str[pos + 1], strlen(str) - pos);
}
gchar *string_insert_char_at(gchar *str, gchar c, int pos) {
gchar *new_str;
int n = strlen(str);
if (str) {
new_str = g_new(gchar, n + 1 + 1); // one for the new char, one for the \0
for (int i = 0, j = 0; j < n + 1 || i < n; i++, j++) {
if (j == pos) {
new_str[j] = c;
j++;
}
new_str[j] = str[i];
}
new_str[n + 1] = str[n];
} else {
new_str = NULL;
}
return new_str;
}