1
0
mirror of https://codeberg.org/vyivel/dulcepan/ synced 2025-06-23 22:14:18 +03:00

10 Commits

Author SHA1 Message Date
60518fe18e config: introduce quick-select-allow-editing
If false while quick-select is true, the left mouse button acts in the same
way as the right mouse button.
2025-03-08 23:41:46 +03:00
b9e94f9584 seat: only save due to quick-select when resizing ends
Moving a selection shouldn't trigger this.
2025-03-08 23:23:18 +03:00
8bc62fdae4 meson: bump version to 1.0.3 2025-03-08 22:56:40 +03:00
accbd01ad6 README.md: bump copyright year 2025-02-07 16:26:15 +03:00
0167c017a0 seat: allow toggling whole output selection
Closes: https://codeberg.org/vyivel/dulcepan/issues/20
2025-02-07 13:25:00 +00:00
9879f3ad69 config: allow to bind multiple keys to the same action
Allows for both Space and Enter to save the selection, for example.
2025-01-31 23:09:41 +03:00
51f744c876 config: unconstify value in loaders 2025-01-31 23:09:33 +03:00
640b015053 util: allow size 0 in dp_zalloc() 2025-01-31 23:09:33 +03:00
0ccb868b39 seat: use crosshair cursor when the whole output is selected
Closes: https://codeberg.org/vyivel/dulcepan/issues/19
2025-01-31 20:07:17 +00:00
7cc26b8e38 select: redraw on interaction start
This fixes a mismatch between the actual selection and its visual
representation when the right mouse button is clicked but not moved
(easier to reproduce with animations disabled).
2025-01-30 22:27:28 +03:00
9 changed files with 204 additions and 71 deletions

View File

@ -30,4 +30,4 @@ GPL-3.0-only
See `LICENSE` for more information.
Copyright © 2024 Kirill Primak
Copyright © 2025 Kirill Primak

View File

@ -33,6 +33,10 @@ animation-duration = 0
# or when a whole output is selected with a mouse button.
quick-select = false
# If true, dulcepan will allow editing the current selection if quick-select is
# true. Has no effect if quick-select is false.
quick-select-allow-editing = true
# If true, dulcepan will remember selection between runs.
# The state is stored at $XDG_CACHE_HOME/dulcepan.
persistence = true
@ -40,6 +44,7 @@ persistence = true
# PNG (zlib) compression level, 0-9
png-compression = 6
# Key bindings
# Key bindings. Each binding is a comma-separated list of key names; empty names
# are ignored. A binding may be empty.
quit-key = Escape
save-key = Space
save-key = Space,Enter

View File

@ -1,7 +1,7 @@
project(
'dulcepan',
'c',
version: '1.0.2',
version: '1.0.3',
license: 'GPL-3.0-only',
default_options: [
'c_std=c11',

View File

@ -15,7 +15,22 @@ static inline void bytes_to_color(uint8_t bytes[static 4], float out[static 4])
}
}
static void load_color(const char *value, int line_idx, float out[static 4]) {
static void keybinding_init(struct dp_keybinding *kb, xkb_keysym_t *syms, size_t n_syms) {
*kb = (struct dp_keybinding){
.syms = dp_zalloc(sizeof(*kb->syms) * n_syms),
.n_syms = n_syms,
};
for (size_t i = 0; i < n_syms; i++) {
xkb_keysym_t sym = syms[i];
kb->syms[i] = sym;
}
}
static void keybinding_finish(struct dp_keybinding *kb) {
free(kb->syms);
}
static void load_color(char *value, int line_idx, float out[static 4]) {
size_t len = strlen(value);
uint8_t bytes[4] = {0, 0, 0, 0};
@ -46,7 +61,7 @@ bad:
dp_log_fatal("Config: invalid color %s on line %d", value, line_idx);
}
static void load_int(const char *value, int line_idx, int min, int max, int *out) {
static void load_int(char *value, int line_idx, int min, int max, int *out) {
const char *p = value;
int mul = 1;
if (*p == '-') {
@ -68,7 +83,7 @@ static void load_int(const char *value, int line_idx, int min, int max, int *out
}
}
static void load_bool(const char *value, int line_idx, bool *out) {
static void load_bool(char *value, int line_idx, bool *out) {
if (strcmp(value, "true") == 0) {
*out = true;
} else if (strcmp(value, "false") == 0) {
@ -78,19 +93,31 @@ static void load_bool(const char *value, int line_idx, bool *out) {
}
}
static void load_key(const char *value, int line_idx, xkb_keysym_t *out) {
*out = xkb_keysym_from_name(value, XKB_KEYSYM_CASE_INSENSITIVE);
if (*out == XKB_KEY_NoSymbol) {
dp_log_fatal("Config: unknown key %s on line %d", value, line_idx);
static void load_key(char *value, int line_idx, struct dp_keybinding *out) {
size_t n_syms = 0;
xkb_keysym_t syms[32];
char *save_ptr = NULL;
for (char *name; (name = strtok_r(value, ",", &save_ptr)) != NULL; value = NULL) {
if (n_syms == sizeof(syms) / sizeof(*syms)) {
// chill out
dp_log_fatal("Config: too many keys on line %d", line_idx);
}
xkb_keysym_t sym = xkb_keysym_from_name(name, XKB_KEYSYM_CASE_INSENSITIVE);
if (sym == XKB_KEY_NoSymbol) {
dp_log_fatal("Config: unknown key \"%s\" on line %d", name, line_idx);
}
syms[n_syms++] = sym;
}
keybinding_finish(out);
keybinding_init(out, syms, n_syms);
}
void dp_config_load(struct dp_state *state, const char *user_path) {
struct dp_config *config = &state->config;
*config = (struct dp_config){
.quit_key = XKB_KEY_Escape,
.save_key = XKB_KEY_space,
.border_size = 2,
.border_gradient = DP_BORDER_GRADIENT_NONE,
.gradient_angle = 45,
@ -98,13 +125,18 @@ void dp_config_load(struct dp_state *state, const char *user_path) {
.animation_duration = 0,
.png_compression = 6,
.quick_select = false,
.quick_select_allow_editing = true,
.persistence = true,
};
bytes_to_color((uint8_t[]){0xff, 0xff, 0xff, 0x40}, config->unselected_color);
bytes_to_color((uint8_t[]){0x00, 0x00, 0x00, 0x00}, config->selected_color);
bytes_to_color((uint8_t[]){0xff, 0xff, 0xff, 0xff}, config->border_color);
bytes_to_color((uint8_t[]){0x00, 0x00, 0x00, 0xff}, config->border_secondary_color);
keybinding_init(&config->quit_kb, (xkb_keysym_t[]){XKB_KEY_Escape}, 1);
keybinding_init(&config->save_kb, (xkb_keysym_t[]){XKB_KEY_space, XKB_KEY_Return}, 2);
FILE *fp = NULL;
if (user_path != NULL) {
fp = fopen(user_path, "r");
@ -212,12 +244,14 @@ void dp_config_load(struct dp_state *state, const char *user_path) {
load_int(value, line_idx, 0, 9, &config->png_compression);
} else if (strcmp(key, "quick-select") == 0) {
load_bool(value, line_idx, &config->quick_select);
} else if (strcmp(key, "quick-select-allow-editing") == 0) {
load_bool(value, line_idx, &config->quick_select_allow_editing);
} else if (strcmp(key, "persistence") == 0) {
load_bool(value, line_idx, &config->persistence);
} else if (strcmp(key, "quit-key") == 0) {
load_key(value, line_idx, &config->quit_key);
load_key(value, line_idx, &config->quit_kb);
} else if (strcmp(key, "save-key") == 0) {
load_key(value, line_idx, &config->save_key);
load_key(value, line_idx, &config->save_kb);
} else {
dp_log_error("Config: unknown key %s on line %d", key, line_idx);
}
@ -226,3 +260,10 @@ void dp_config_load(struct dp_state *state, const char *user_path) {
fclose(fp);
}
void dp_config_finish(struct dp_state *state) {
struct dp_config *config = &state->config;
keybinding_finish(&config->quit_kb);
keybinding_finish(&config->save_kb);
}

View File

@ -126,6 +126,14 @@ struct dp_selection {
int resize_edges;
// Resize anchor
double resize_x, resize_y;
// Used for toggling
struct {
// If not NULL, the current output has been selected via the dedicated
// action rather than by persistent state loading or manual resizing
struct dp_output *output;
double x, y, width, height;
} last_partial;
};
enum dp_file_format {
@ -141,6 +149,11 @@ enum dp_border_gradient {
DP_BORDER_GRADIENT_LOOP,
};
struct dp_keybinding {
xkb_keysym_t *syms;
size_t n_syms;
};
struct dp_config {
// RGBA, not premultiplied
float unselected_color[4];
@ -148,8 +161,8 @@ struct dp_config {
float border_color[4];
float border_secondary_color[4];
xkb_keysym_t quit_key;
xkb_keysym_t save_key;
struct dp_keybinding quit_kb;
struct dp_keybinding save_kb;
int border_size; // 0 if disabled
enum dp_border_gradient border_gradient;
@ -158,7 +171,10 @@ struct dp_config {
int animation_duration; // In milliseconds
int png_compression;
bool quick_select;
bool quick_select_allow_editing;
bool persistence;
};
@ -195,6 +211,8 @@ struct dp_state {
const char *output_path; // May be NULL
enum dp_file_format output_format;
bool allow_selection_editing;
bool show_cursors;
cairo_pattern_t *border_pattern;
@ -204,6 +222,7 @@ struct dp_state {
};
void dp_config_load(struct dp_state *state, const char *user_path);
void dp_config_finish(struct dp_state *state);
// When done, data must be unmapped
struct wl_buffer *dp_buffer_create(struct dp_state *state, int32_t width, int32_t height,
@ -226,7 +245,7 @@ void dp_select_stop_interactive(struct dp_selection *selection);
void dp_select_notify_pointer_position(
struct dp_selection *selection, struct dp_output *output, double x, double y);
void dp_select_whole(struct dp_selection *selection, struct dp_output *output);
void dp_select_toggle_whole(struct dp_selection *selection, struct dp_output *output);
void dp_save(struct dp_state *state);

View File

@ -176,7 +176,7 @@ static void help(const char *prog) {
"\n"
"Use the left or right mouse button to select a part of the output. A selection\n"
"can also be moved and resized with the left mouse button. Clicking the middle\n"
"mouse button selects the entire output.\n",
"mouse button toggles the selection of the entire output.\n",
prog);
}
@ -248,6 +248,9 @@ int main(int argc, char **argv) {
state.xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
struct dp_config *config = &state.config;
state.allow_selection_editing = !config->quick_select || config->quick_select_allow_editing;
if (config->border_gradient != DP_BORDER_GRADIENT_NONE) {
state.border_pattern = cairo_pattern_create_linear(0, 0, 1, 0);
cairo_pattern_add_color_stop_rgba(state.border_pattern, 0, config->border_color[0],
@ -278,6 +281,8 @@ int main(int argc, char **argv) {
run(&state);
dp_config_finish(&state);
cairo_pattern_destroy(state.border_pattern);
xkb_context_unref(state.xkb_context);

View File

@ -50,6 +50,15 @@ static void keyboard_handle_leave(
// Ignored
}
static bool match_keybinding(struct dp_keybinding *kb, uint32_t sym) {
for (size_t i = 0; i < kb->n_syms; i++) {
if (sym == kb->syms[i]) {
return true;
}
}
return false;
}
static void keyboard_handle_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial,
uint32_t time_msec, uint32_t keycode, enum wl_keyboard_key_state key_state) {
struct dp_seat *seat = data;
@ -62,9 +71,9 @@ static void keyboard_handle_key(void *data, struct wl_keyboard *wl_keyboard, uin
struct dp_state *state = seat->state;
struct dp_config *config = &state->config;
if (keysym == config->quit_key) {
if (match_keybinding(&config->quit_kb, keysym)) {
state->status = DP_STATUS_QUIT;
} else if (keysym == config->save_key) {
} else if (match_keybinding(&config->save_kb, keysym)) {
state->status = DP_STATUS_SAVED;
}
}
@ -83,50 +92,55 @@ static const struct wl_keyboard_listener keyboard_listener = {
.modifiers = keyboard_handle_modifiers,
};
static enum wp_cursor_shape_device_v1_shape get_cursor_shape(struct dp_selection *selection) {
switch (selection->action) {
case DP_SELECTION_ACTION_NONE:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR;
case DP_SELECTION_ACTION_RESIZING:
if (selection->width == 0 || selection->height == 0) {
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR;
static enum wp_cursor_shape_device_v1_shape get_cursor_shape(struct dp_state *state) {
if (state->allow_selection_editing) {
struct dp_selection *selection = &state->selection;
switch (selection->action) {
case DP_SELECTION_ACTION_NONE:
break;
case DP_SELECTION_ACTION_RESIZING:
if (selection->width == 0 || selection->height == 0) {
break;
}
switch (selection->resize_edges) {
case DP_EDGE_TOP:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE;
case DP_EDGE_TOP | DP_EDGE_LEFT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE;
case DP_EDGE_TOP | DP_EDGE_RIGHT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE;
case DP_EDGE_BOTTOM:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE;
case DP_EDGE_BOTTOM | DP_EDGE_LEFT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE;
case DP_EDGE_BOTTOM | DP_EDGE_RIGHT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE;
case DP_EDGE_LEFT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE;
case DP_EDGE_RIGHT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE;
}
break;
case DP_SELECTION_ACTION_MOVING:
// XXX: this might have rounding issues but whatever
if (selection->x == 0 && selection->y == 0 &&
selection->width == selection->output->effective_width &&
selection->height == selection->output->effective_height) {
// Moving is impossible
break;
}
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE;
}
switch (selection->resize_edges) {
case DP_EDGE_TOP:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE;
case DP_EDGE_TOP | DP_EDGE_LEFT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE;
case DP_EDGE_TOP | DP_EDGE_RIGHT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE;
case DP_EDGE_BOTTOM:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE;
case DP_EDGE_BOTTOM | DP_EDGE_LEFT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE;
case DP_EDGE_BOTTOM | DP_EDGE_RIGHT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE;
case DP_EDGE_LEFT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE;
case DP_EDGE_RIGHT:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE;
}
break;
case DP_SELECTION_ACTION_MOVING:
// XXX: this might have rounding issues but whatever
if (selection->x == 0 && selection->y == 0 &&
selection->width == selection->output->effective_width &&
selection->height == selection->output->effective_height) {
// Moving is impossible
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
}
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE;
}
abort(); // Unreachable
// The default cursor
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR;
}
static void update_cursor(struct dp_seat *seat) {
if (seat->cursor_shape_device != NULL) {
wp_cursor_shape_device_v1_set_shape(seat->cursor_shape_device, seat->pointer_serial,
get_cursor_shape(&seat->state->selection));
wp_cursor_shape_device_v1_set_shape(
seat->cursor_shape_device, seat->pointer_serial, get_cursor_shape(seat->state));
}
}
@ -172,7 +186,8 @@ static void pointer_handle_button(void *data, struct wl_pointer *wl_pointer, uin
struct dp_selection *selection = &state->selection;
if (button_state != WL_POINTER_BUTTON_STATE_PRESSED) {
if (selection->width > 0 && selection->height > 0 && state->config.quick_select) {
if (selection->action == DP_SELECTION_ACTION_RESIZING && selection->width > 0 &&
selection->height > 0 && state->config.quick_select) {
state->status = DP_STATUS_SAVED;
}
dp_select_stop_interactive(selection);
@ -186,14 +201,14 @@ static void pointer_handle_button(void *data, struct wl_pointer *wl_pointer, uin
switch (button) {
case BTN_LEFT:
case BTN_RIGHT:
dp_select_start_interactive(
selection, seat->ptr_output, seat->ptr_x, seat->ptr_y, button == BTN_LEFT);
dp_select_start_interactive(selection, seat->ptr_output, seat->ptr_x, seat->ptr_y,
state->allow_selection_editing && button == BTN_LEFT);
update_cursor(seat);
break;
case BTN_MIDDLE:
dp_select_whole(selection, seat->ptr_output);
dp_select_toggle_whole(selection, seat->ptr_output);
// dp_select_whole() doesn't invalidate the interactive state, so do it manually
// Update the interaction state manually
dp_select_notify_pointer_position(selection, seat->ptr_output, seat->ptr_x, seat->ptr_y);
update_cursor(seat);

View File

@ -20,6 +20,24 @@ static void set_selected_output(struct dp_selection *selection, struct dp_output
}
}
static inline bool has_last_partial(struct dp_selection *selection) {
return selection->last_partial.output != NULL;
}
static inline void reset_last_partial(struct dp_selection *selection) {
selection->last_partial.output = NULL;
}
static bool has_partial(struct dp_selection *selection) {
struct dp_output *output = selection->output;
if (output == NULL) {
return false;
}
return selection->x != 0 || selection->y != 0 || selection->width != output->effective_width ||
selection->height != output->effective_height;
}
static void update_action(
struct dp_selection *selection, struct dp_output *output, double x, double y) {
if (output == selection->output) {
@ -161,6 +179,8 @@ void dp_select_start_interactive(struct dp_selection *selection, struct dp_outpu
update_action(selection, output, x, y);
selection->action_active = true;
reset_last_partial(selection);
if (modify_existing) {
switch (selection->action) {
case DP_SELECTION_ACTION_NONE:
@ -187,6 +207,8 @@ void dp_select_start_interactive(struct dp_selection *selection, struct dp_outpu
selection->resize_edges = DP_EDGE_BOTTOM | DP_EDGE_RIGHT;
init_resize(selection, x, y);
dp_output_redraw(output);
}
void dp_select_stop_interactive(struct dp_selection *selection) {
@ -211,13 +233,35 @@ void dp_select_notify_pointer_position(
}
}
void dp_select_whole(struct dp_selection *selection, struct dp_output *output) {
set_selected_output(selection, output);
void dp_select_toggle_whole(struct dp_selection *selection, struct dp_output *output) {
if (selection->output == output && has_last_partial(selection)) {
// Toggle the selection back to the last partial one
set_selected_output(selection, selection->last_partial.output);
selection->x = 0;
selection->y = 0;
selection->width = output->effective_width;
selection->height = output->effective_height;
selection->x = selection->last_partial.x;
selection->y = selection->last_partial.y;
selection->width = selection->last_partial.width;
selection->height = selection->last_partial.height;
dp_output_redraw(output);
reset_last_partial(selection);
} else {
// Don't save another whole output selection as partial
if (has_partial(selection) && !has_last_partial(selection)) {
selection->last_partial.output = selection->output;
selection->last_partial.x = selection->x;
selection->last_partial.y = selection->y;
selection->last_partial.width = selection->width;
selection->last_partial.height = selection->height;
}
set_selected_output(selection, output);
selection->x = 0;
selection->y = 0;
selection->width = output->effective_width;
selection->height = output->effective_height;
}
dp_output_redraw(selection->output);
}

View File

@ -28,6 +28,10 @@ void dp_log_fatal(const char *fmt, ...) {
}
void *dp_zalloc(size_t size) {
if (size == 0) {
return NULL;
}
void *ptr = calloc(1, size);
if (ptr == NULL) {
dp_log_fatal("Failed to allocate %zu bytes", size);