diff --git a/dulcepan.cfg b/dulcepan.cfg index c73e401..e4408ec 100644 --- a/dulcepan.cfg +++ b/dulcepan.cfg @@ -4,10 +4,31 @@ unselected-color = ffffff40 selected-color = 00000000 border-color = ffffff +border-secondary-color = 000000 # 0 to disable borders border-size = 2 +# Border gradient type: +# - none: only the primary border color is used +# - linear: uses a linear gradient relative to the selection +# - loop: uses a repeated linear gradient +border-gradient = none + +# For "linear" gradient mode +# Counterclockwise, in degrees +gradient-angle = 45 + +# For "loop" gradient mode +# The distance between gradient stops +loop-step = 100 + +# For "linear" and "loop" gradient modes +# In milliseconds, 0 to disable animation +# With "linear" mode: the time it takes for the pattern to make one full turn +# With "loop" mode: the time it takes for the pattern to move by double the loop step +animation-duration = 0 + # If true, dulcepan will save immediately when interactive selection is stopped # or when a whole output is selected with a mouse button. quick-select = false diff --git a/src/config.c b/src/config.c index e93019a..0b35fe5 100644 --- a/src/config.c +++ b/src/config.c @@ -92,12 +92,17 @@ void dp_config_load(struct dp_state *state, const char *user_path) { .quit_key = XKB_KEY_Escape, .save_key = XKB_KEY_space, .border_size = 2, + .border_gradient = DP_BORDER_GRADIENT_NONE, + .gradient_angle = 45, + .loop_step = 100, + .animation_duration = 0, .png_compression = 6, .quick_select = false, }; 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); FILE *fp = NULL; if (user_path != NULL) { @@ -180,8 +185,28 @@ void dp_config_load(struct dp_state *state, const char *user_path) { load_color(value, line_idx, config->selected_color); } else if (strcmp(key, "border-color") == 0) { load_color(value, line_idx, config->border_color); + } else if (strcmp(key, "border-secondary-color") == 0) { + load_color(value, line_idx, config->border_secondary_color); } else if (strcmp(key, "border-size") == 0) { load_int(value, line_idx, 0, INT_MAX, &config->border_size); + } else if (strcmp(key, "border-gradient") == 0) { + if (strcmp(value, "none") == 0) { + config->border_gradient = DP_BORDER_GRADIENT_NONE; + } else if (strcmp(value, "linear") == 0) { + config->border_gradient = DP_BORDER_GRADIENT_LINEAR; + } else if (strcmp(value, "loop") == 0) { + config->border_gradient = DP_BORDER_GRADIENT_LOOP; + } else { + dp_log_fatal("Config: invalid border-gradient %s on line %d", value, line_idx); + } + } else if (strcmp(key, "gradient-angle") == 0) { + int deg; + load_int(value, line_idx, INT_MIN, INT_MAX, °); + config->gradient_angle = (double)deg * DP_PI / 180; + } else if (strcmp(key, "loop-step") == 0) { + load_int(value, line_idx, 1, INT_MAX, &config->loop_step); + } else if (strcmp(key, "animation-duration") == 0) { + load_int(value, line_idx, 0, INT_MAX, &config->animation_duration); } else if (strcmp(key, "png-compression") == 0) { load_int(value, line_idx, 0, 9, &config->png_compression); } else if (strcmp(key, "quick-select") == 0) { diff --git a/src/dulcepan.h b/src/dulcepan.h index fd67567..5530317 100644 --- a/src/dulcepan.h +++ b/src/dulcepan.h @@ -8,6 +8,10 @@ #include #include +#define DP_PI 3.14159265358979323846 +// sqrt(2) / 2 +#define DP_SQRT2_2 0.7071067811865476 + // Per-output #define DP_SWAPCHAIN_LEN 2 @@ -128,14 +132,28 @@ enum dp_file_format { DP_FILE_PPM, }; +enum dp_border_gradient { + DP_BORDER_GRADIENT_NONE, + DP_BORDER_GRADIENT_LINEAR, + DP_BORDER_GRADIENT_LOOP, +}; + struct dp_config { // RGBA, not premultiplied float unselected_color[4]; float selected_color[4]; float border_color[4]; + float border_secondary_color[4]; + xkb_keysym_t quit_key; xkb_keysym_t save_key; + int border_size; // 0 if disabled + enum dp_border_gradient border_gradient; + double gradient_angle; // In radians + int loop_step; + int animation_duration; // In milliseconds + int png_compression; bool quick_select; }; @@ -172,6 +190,11 @@ struct dp_state { enum dp_file_format output_format; bool show_cursors; + + cairo_pattern_t *border_pattern; + cairo_matrix_t precomputed_linear_matrix; + double loop_step_scale; + double time_multiplier; }; void dp_config_load(struct dp_state *state, const char *user_path); @@ -207,6 +230,8 @@ void dp_log_fatal(const char *fmt, ...); void *dp_zalloc(size_t size); char *dp_strdup(const char *str); +void dp_matrix_compute_linear(cairo_matrix_t *matrix, double angle); + const char *dp_ext_from_path(const char *path); enum dp_file_format dp_ext_to_format(const char *ext); diff --git a/src/main.c b/src/main.c index 8a0fcf1..6d16bc2 100644 --- a/src/main.c +++ b/src/main.c @@ -222,15 +222,46 @@ int main(int argc, char **argv) { wl_list_init(&state.outputs); wl_list_init(&state.seats); - state.xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - state.display = wl_display_connect(NULL); if (state.display == NULL) { dp_log_fatal("Failed to connect to a Wayland compositor"); } + state.xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + + struct dp_config *config = &state.config; + 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], + config->border_color[1], config->border_color[2], config->border_color[3]); + cairo_pattern_add_color_stop_rgba(state.border_pattern, 1, + config->border_secondary_color[0], config->border_secondary_color[1], + config->border_secondary_color[2], config->border_secondary_color[3]); + + switch (config->border_gradient) { + case DP_BORDER_GRADIENT_NONE: + abort(); // Unreachable + case DP_BORDER_GRADIENT_LINEAR: + if (config->animation_duration != 0) { + state.time_multiplier = DP_PI * 2.0 / config->animation_duration; + } else { + dp_matrix_compute_linear(&state.precomputed_linear_matrix, config->gradient_angle); + } + break; + case DP_BORDER_GRADIENT_LOOP: + cairo_pattern_set_extend(state.border_pattern, CAIRO_EXTEND_REFLECT); + state.loop_step_scale = 1.0 / config->loop_step; + if (config->animation_duration != 0) { + state.time_multiplier = 2.0 / config->animation_duration; + } + break; + } + } + run(&state); + cairo_pattern_destroy(state.border_pattern); + xkb_context_unref(state.xkb_context); wl_display_disconnect(state.display); diff --git a/src/output.c b/src/output.c index 91132b1..6932752 100644 --- a/src/output.c +++ b/src/output.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include "dulcepan.h" @@ -265,6 +266,56 @@ static inline void set_cairo_color(cairo_t *cairo, float color[static 4]) { cairo_set_source_rgba(cairo, color[0], color[1], color[2], color[3]); } +static inline uint32_t get_time_msec(void) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (uint32_t)(now.tv_sec * 1000 + now.tv_nsec / 1000000); +} + +static void setup_linear_gradient(cairo_t *cairo, struct dp_state *state) { + struct dp_selection *selection = &state->selection; + struct dp_config *config = &state->config; + + cairo_matrix_t matrix; + + if (config->animation_duration != 0) { + double angle = (double)get_time_msec() * state->time_multiplier; + dp_matrix_compute_linear(&matrix, angle); + } else { + matrix = state->precomputed_linear_matrix; + } + + cairo_matrix_scale(&matrix, 1.0 / selection->width, 1.0 / selection->height); + cairo_matrix_translate(&matrix, -selection->x, -selection->y); + + cairo_pattern_set_matrix(state->border_pattern, &matrix); + cairo_set_source(cairo, state->border_pattern); +} + +static void setup_loop_gradient(cairo_t *cairo, struct dp_state *state) { + struct dp_selection *selection = &state->selection; + struct dp_config *config = &state->config; + + double scale = state->loop_step_scale / selection->output->scale; + + double offset = + (selection->x + selection->y + (selection->width + selection->height) / 2.0) * scale; + if (config->animation_duration != 0) { + offset += (double)get_time_msec() * state->time_multiplier / selection->output->scale; + } + + cairo_matrix_t matrix; + cairo_matrix_init(&matrix, scale, 0, 0, scale, fmod(offset, 2.0), 0); + + // Rotate by 45° + cairo_matrix_t rotation; + cairo_matrix_init(&rotation, -DP_SQRT2_2, DP_SQRT2_2, -DP_SQRT2_2, -DP_SQRT2_2, 0, 0); + cairo_matrix_multiply(&matrix, &rotation, &matrix); + + cairo_pattern_set_matrix(state->border_pattern, &matrix); + cairo_set_source(cairo, state->border_pattern); +} + static void redraw(struct dp_output *output) { assert(output->redraw_callback == NULL); @@ -282,6 +333,8 @@ static void redraw(struct dp_output *output) { } buffer->used = true; + output->needs_redraw = false; + struct dp_state *state = output->state; struct dp_selection *selection = &state->selection; struct dp_config *config = &state->config; @@ -299,7 +352,19 @@ static void redraw(struct dp_output *output) { double off = scaled_size / 2.0; cairo_rectangle(buffer->cairo, selection->x - off, selection->y - off, selection->width + scaled_size, selection->height + scaled_size); - set_cairo_color(buffer->cairo, config->border_color); + + switch (config->border_gradient) { + case DP_BORDER_GRADIENT_NONE: + set_cairo_color(buffer->cairo, config->border_color); + break; + case DP_BORDER_GRADIENT_LINEAR: + setup_linear_gradient(buffer->cairo, state); + break; + case DP_BORDER_GRADIENT_LOOP: + setup_loop_gradient(buffer->cairo, state); + break; + } + cairo_stroke(buffer->cairo); } @@ -307,6 +372,10 @@ static void redraw(struct dp_output *output) { buffer->cairo, selection->x, selection->y, selection->width, selection->height); set_cairo_color(buffer->cairo, config->selected_color); cairo_fill(buffer->cairo); + + if (config->border_gradient != DP_BORDER_GRADIENT_NONE && config->animation_duration != 0) { + output->needs_redraw = true; + } } wl_surface_attach(output->select_surface, buffer->wl_buffer, 0, 0); @@ -315,7 +384,6 @@ static void redraw(struct dp_output *output) { output->redraw_callback = wl_surface_frame(output->select_surface); wl_callback_add_listener(output->redraw_callback, &redraw_callback_listener, output); - output->needs_redraw = false; wl_surface_commit(output->select_surface); } diff --git a/src/util.c b/src/util.c index 94790e9..f00d33a 100644 --- a/src/util.c +++ b/src/util.c @@ -63,3 +63,17 @@ enum dp_file_format dp_ext_to_format(const char *ext) { } return DP_FILE_UNKNOWN; } + +void dp_matrix_compute_linear(cairo_matrix_t *matrix, double angle) { + double c = cos(angle), s = sin(angle); + double f = 1.0 / (fabs(c) + fabs(s)); + + cairo_matrix_init_translate(matrix, 0.5, 0.5); + cairo_matrix_scale(matrix, f, 1); + + cairo_matrix_t tmp; + cairo_matrix_init(&tmp, c, s, -s, c, 0, 0); + cairo_matrix_multiply(matrix, &tmp, matrix); + + cairo_matrix_translate(matrix, -0.5, -0.5); +}