mirror of
https://github.com/jtheoof/swappy.git
synced 2024-08-17 02:30:47 +03:00
feat(paint): introduce text paint
This commit is contained in:
parent
87fd4db304
commit
3347bf23bf
@ -95,6 +95,7 @@ Install dependencies:
|
||||
- meson
|
||||
- wayland
|
||||
- cairo
|
||||
- pango
|
||||
- gtk
|
||||
|
||||
Optional dependencies:
|
||||
|
@ -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);
|
||||
|
@ -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
7
include/util.h
Normal 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);
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
131
src/paint.c
131
src/paint.c
@ -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 {
|
||||
|
73
src/render.c
73
src/render.c
@ -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
32
src/util.c
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user