brewery
notashelf /
a82b986ac6bbb4c02515ebdb4a5c87dd49ba68d4

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git

Commit a82b986ac6bb

tarball

NotAShelf <raf@notashelf.dev> · 2026-04-15 08:55 UTC

unverified4 files changed+189-29
{render,config}: allow specifying wallpaper anchor position

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Iee73e9d149e85d2c00eaba4be25d42bd6a6a6964
diff --git a/include/chroma.h b/include/chroma.hindex 6c8ed6e..76afc43 100644--- a/include/chroma.h+++ b/include/chroma.h@@ -44,6 +44,19 @@ typedef enum {   CHROMA_SCALE_CENTER = 3   // center image at original size } chroma_scale_mode_t; +// Anchor positions for wallpaper placement+typedef enum {+  CHROMA_ANCHOR_CENTER = 0,      // center of the output (default)+  CHROMA_ANCHOR_TOP = 1,         // top edge, centered horizontally+  CHROMA_ANCHOR_BOTTOM = 2,      // bottom edge, centered horizontally+  CHROMA_ANCHOR_LEFT = 3,        // left edge, centered vertically+  CHROMA_ANCHOR_RIGHT = 4,       // right edge, centered vertically+  CHROMA_ANCHOR_TOP_LEFT = 5,    // top-left corner+  CHROMA_ANCHOR_TOP_RIGHT = 6,   // top-right corner+  CHROMA_ANCHOR_BOTTOM_LEFT = 7, // bottom-left corner+  CHROMA_ANCHOR_BOTTOM_RIGHT = 8 // bottom-right corner+} chroma_anchor_t;+ // Image filtering quality settings typedef enum {   CHROMA_FILTER_NEAREST = 0,  // nearest neighbor filtering (pixelated)@@ -90,6 +103,7 @@ typedef struct {   // Configuration for this output   chroma_scale_mode_t scale_mode;   chroma_filter_quality_t filter_quality;+  chroma_anchor_t anchor;   bool config_loaded;    // OpenGL resource cache@@ -108,6 +122,7 @@ typedef struct {   char image_path[MAX_PATH_LEN];   chroma_scale_mode_t scale_mode;   chroma_filter_quality_t filter_quality;+  chroma_anchor_t anchor; } chroma_config_mapping_t;  // Application configuration@@ -120,6 +135,7 @@ typedef struct {   // Global scaling and filtering settings (used as defaults)   chroma_scale_mode_t default_scale_mode;   chroma_filter_quality_t default_filter_quality;+  chroma_anchor_t default_anchor;    // Image downsampling settings   bool enable_downsampling; // enable automatic downsampling@@ -231,7 +247,8 @@ const char *chroma_config_get_image_for_output(chroma_config_t *config,                                                const char *output_name); int chroma_config_get_mapping_for_output(     chroma_config_t *config, const char *output_name,-    chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality);+    chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality,+    chroma_anchor_t *anchor);  void chroma_config_print(const chroma_config_t *config); diff --git a/src/config.c b/src/config.cindex 619af3a..0a35467 100644--- a/src/config.c+++ b/src/config.c@@ -6,8 +6,6 @@  #include "../include/chroma.h" -// Default configuration values- static char *trim_whitespace(char *str) {   char *end; @@ -122,11 +120,71 @@ static const char *filter_quality_to_string(chroma_filter_quality_t quality) {   } } +// Parse anchor position from string+static chroma_anchor_t parse_anchor(const char *value) {+  if (!value)+    return CHROMA_ANCHOR_CENTER;++  if (strcasecmp(value, "center") == 0) {+    return CHROMA_ANCHOR_CENTER;+  } else if (strcasecmp(value, "top") == 0) {+    return CHROMA_ANCHOR_TOP;+  } else if (strcasecmp(value, "bottom") == 0) {+    return CHROMA_ANCHOR_BOTTOM;+  } else if (strcasecmp(value, "left") == 0) {+    return CHROMA_ANCHOR_LEFT;+  } else if (strcasecmp(value, "right") == 0) {+    return CHROMA_ANCHOR_RIGHT;+  } else if (strcasecmp(value, "top-left") == 0 ||+             strcasecmp(value, "topleft") == 0) {+    return CHROMA_ANCHOR_TOP_LEFT;+  } else if (strcasecmp(value, "top-right") == 0 ||+             strcasecmp(value, "topright") == 0) {+    return CHROMA_ANCHOR_TOP_RIGHT;+  } else if (strcasecmp(value, "bottom-left") == 0 ||+             strcasecmp(value, "bottomleft") == 0) {+    return CHROMA_ANCHOR_BOTTOM_LEFT;+  } else if (strcasecmp(value, "bottom-right") == 0 ||+             strcasecmp(value, "bottomright") == 0) {+    return CHROMA_ANCHOR_BOTTOM_RIGHT;+  }++  chroma_log("WARN", "Unknown anchor: %s (using center)", value);+  return CHROMA_ANCHOR_CENTER;+}++// Get string representation of anchor position+static const char *anchor_to_string(chroma_anchor_t anchor) {+  switch (anchor) {+  case CHROMA_ANCHOR_CENTER:+    return "center";+  case CHROMA_ANCHOR_TOP:+    return "top";+  case CHROMA_ANCHOR_BOTTOM:+    return "bottom";+  case CHROMA_ANCHOR_LEFT:+    return "left";+  case CHROMA_ANCHOR_RIGHT:+    return "right";+  case CHROMA_ANCHOR_TOP_LEFT:+    return "top-left";+  case CHROMA_ANCHOR_TOP_RIGHT:+    return "top-right";+  case CHROMA_ANCHOR_BOTTOM_LEFT:+    return "bottom-left";+  case CHROMA_ANCHOR_BOTTOM_RIGHT:+    return "bottom-right";+  default:+    return "unknown";+  }+}+ // Output-to-image mapping static int add_output_mapping(chroma_config_t *config, const char *output_name,                               const char *image_path,                               chroma_scale_mode_t scale_mode,-                              chroma_filter_quality_t filter_quality) {+                              chroma_filter_quality_t filter_quality,+                              chroma_anchor_t anchor) {   if (!config || !output_name || !image_path) {     return CHROMA_ERROR_INIT;   }@@ -159,12 +217,14 @@ static int add_output_mapping(chroma_config_t *config, const char *output_name,   strcpy(mapping->image_path, image_path);   mapping->scale_mode = scale_mode;   mapping->filter_quality = filter_quality;+  mapping->anchor = anchor;    config->mapping_count++; -  chroma_log("DEBUG", "Added mapping: %s -> %s (scale: %s, filter: %s)",-             output_name, image_path, scale_mode_to_string(scale_mode),-             filter_quality_to_string(filter_quality));+  chroma_log(+      "DEBUG", "Added mapping: %s -> %s (scale: %s, filter: %s, anchor: %s)",+      output_name, image_path, scale_mode_to_string(scale_mode),+      filter_quality_to_string(filter_quality), anchor_to_string(anchor));   chroma_log("TRACE", "Output mapping %d: '%s' -> '%s' (path length: %zu)",              config->mapping_count, output_name, image_path, path_len);   return CHROMA_OK;@@ -183,6 +243,7 @@ static void init_default_config(chroma_config_t *config) {   // Set default scaling and filtering   config->default_scale_mode = CHROMA_SCALE_FILL;   config->default_filter_quality = CHROMA_FILTER_LINEAR;+  config->default_anchor = CHROMA_ANCHOR_CENTER;    // Set default downsampling settings   config->enable_downsampling = true; // enable by default, performance etc.@@ -286,6 +347,10 @@ static int parse_config_line(chroma_config_t *config, char *line,     config->enable_downsampling = parse_bool(value);     chroma_log("DEBUG", "Set downsampling: %s",                config->enable_downsampling ? "enabled" : "disabled");+  } else if (strcasecmp(key, "anchor") == 0) {+    config->default_anchor = parse_anchor(value);+    chroma_log("DEBUG", "Set default anchor: %s",+               anchor_to_string(config->default_anchor));   } else if (strcasecmp(key, "max_output_width") == 0) {     int width = atoi(value);     if (width > 0 && width <= 16384) { // Reasonable limits@@ -351,6 +416,10 @@ static int parse_config_line(chroma_config_t *config, char *line,         mapping->filter_quality = parse_filter_quality(value);         chroma_log("DEBUG", "Set filter quality for output %s: %s", output_name,                    filter_quality_to_string(mapping->filter_quality));+      } else if (strcasecmp(property, "anchor") == 0) {+        mapping->anchor = parse_anchor(value);+        chroma_log("DEBUG", "Set anchor for output %s: %s", output_name,+                   anchor_to_string(mapping->anchor));       } else {         chroma_log("WARN", "Unknown output property: %s (line %d)", property,                    line_number);@@ -375,7 +444,8 @@ static int parse_config_line(chroma_config_t *config, char *line,      if (add_output_mapping(config, output_name, path_to_validate,                            config->default_scale_mode,-                           config->default_filter_quality) != CHROMA_OK) {+                           config->default_filter_quality,+                           config->default_anchor) != CHROMA_OK) {       chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name,                  path_to_validate);       if (expanded_path) {@@ -518,12 +588,13 @@ const char *chroma_config_get_image_for_output(chroma_config_t *config,   return NULL; } -// Get configuration mapping for output, including scale mode and filter-// quality+// Get configuration mapping for output, including scale mode, filter+// quality, and anchor int chroma_config_get_mapping_for_output(     chroma_config_t *config, const char *output_name,-    chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality) {-  if (!config || !output_name || !scale_mode || !filter_quality) {+    chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality,+    chroma_anchor_t *anchor) {+  if (!config || !output_name || !scale_mode || !filter_quality || !anchor) {     return CHROMA_ERROR_INIT;   } @@ -532,10 +603,13 @@ int chroma_config_get_mapping_for_output(     if (strcmp(config->mappings[i].output_name, output_name) == 0) {       *scale_mode = config->mappings[i].scale_mode;       *filter_quality = config->mappings[i].filter_quality;+      *anchor = config->mappings[i].anchor;       chroma_log("DEBUG",-                 "Found specific mapping for output %s: scale=%s, filter=%s",+                 "Found specific mapping for output %s: scale=%s, filter=%s, "+                 "anchor=%s",                  output_name, scale_mode_to_string(*scale_mode),-                 filter_quality_to_string(*filter_quality));+                 filter_quality_to_string(*filter_quality),+                 anchor_to_string(*anchor));       return CHROMA_OK;     }   }@@ -543,14 +617,14 @@ int chroma_config_get_mapping_for_output(   // Return defaults if no specific mapping found   *scale_mode = config->default_scale_mode;   *filter_quality = config->default_filter_quality;-  chroma_log("DEBUG", "Using defaults for output %s: scale=%s, filter=%s",-             output_name, scale_mode_to_string(*scale_mode),-             filter_quality_to_string(*filter_quality));+  *anchor = config->default_anchor;+  chroma_log(+      "DEBUG", "Using defaults for output %s: scale=%s, filter=%s, anchor=%s",+      output_name, scale_mode_to_string(*scale_mode),+      filter_quality_to_string(*filter_quality), anchor_to_string(*anchor));   return CHROMA_OK; } -- // Print current configuration for debugging void chroma_config_print(const chroma_config_t *config) {   if (!config) {diff --git a/src/core.c b/src/core.cindex d84887f..a8890c1 100644--- a/src/core.c+++ b/src/core.c@@ -112,19 +112,21 @@ static int assign_wallpaper_to_output(chroma_state_t *state,   // Store old configuration values for comparison   chroma_scale_mode_t old_scale_mode = output->scale_mode;   chroma_filter_quality_t old_filter_quality = output->filter_quality;+  chroma_anchor_t old_anchor = output->anchor;   bool had_config = output->config_loaded; -  // Load configuration for this output (scale mode and filter quality)+  // Load configuration for this output (scale mode, filter quality, anchor)   if (chroma_config_get_mapping_for_output(           &state->config, output->name ? output->name : "unknown",-          &output->scale_mode, &output->filter_quality) == CHROMA_OK) {+          &output->scale_mode, &output->filter_quality, &output->anchor) == CHROMA_OK) {     output->config_loaded = true;-    chroma_log("DEBUG", "Loaded config for output %u: scale=%d, filter=%d",-               output->id, output->scale_mode, output->filter_quality);+    chroma_log("DEBUG", "Loaded config for output %u: scale=%d, filter=%d, anchor=%d",+               output->id, output->scale_mode, output->filter_quality, output->anchor);      // Check if configuration changed and invalidate texture if needed     if (had_config && (old_scale_mode != output->scale_mode ||-                       old_filter_quality != output->filter_quality)) {+                       old_filter_quality != output->filter_quality ||+                       old_anchor != output->anchor)) {       chroma_output_invalidate_texture(output);       output->vbo_dirty = true; // VBO needs update for new scale mode       chroma_log("DEBUG",diff --git a/src/render.c b/src/render.cindex f4c4a3f..281489f 100644--- a/src/render.c+++ b/src/render.c@@ -41,8 +41,9 @@ static void get_gl_filter_params(chroma_filter_quality_t quality,   } } -// Calculate texture coordinates based on scaling mode+// Calculate texture coordinates based on scaling mode and anchor position static void calculate_texture_coords(chroma_scale_mode_t scale_mode,+                                     chroma_anchor_t anchor,                                      int image_width, int image_height,                                      int output_width, int output_height,                                      float tex_coords[8]) {@@ -142,6 +143,72 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode,     break;   } +  // Apply anchor-based offset by shifting the visible crop region+  // For fill: shift which part of image is shown+  // For fit/center: shift where the image is positioned+  // +  // Positive shift reveals left/bottom portion+  // Negative shift reveals right/top portion+  float u_shift = 0.0f;+  float v_shift = 0.0f;++  switch (anchor) {+  case CHROMA_ANCHOR_CENTER:+    u_shift = 0.0f;+    v_shift = 0.0f;+    break;+  case CHROMA_ANCHOR_TOP:+    u_shift = 0.0f;+    v_shift = -0.5f; // shift up to show top of image+    break;+  case CHROMA_ANCHOR_BOTTOM:+    u_shift = 0.0f;+    v_shift = 0.5f; // shift down to show bottom of image+    break;+  case CHROMA_ANCHOR_LEFT:+    u_shift = -0.5f; // shift left to show left of image+    v_shift = 0.0f;+    break;+  case CHROMA_ANCHOR_RIGHT:+    u_shift = 0.5f; // shift right to show right of image+    v_shift = 0.0f;+    break;+  case CHROMA_ANCHOR_TOP_LEFT:+    u_shift = -0.5f;+    v_shift = -0.5f;+    break;+  case CHROMA_ANCHOR_TOP_RIGHT:+    u_shift = 0.5f;+    v_shift = -0.5f;+    break;+  case CHROMA_ANCHOR_BOTTOM_LEFT:+    u_shift = -0.5f;+    v_shift = 0.5f;+    break;+  case CHROMA_ANCHOR_BOTTOM_RIGHT:+    u_shift = 0.5f;+    v_shift = 0.5f;+    break;+  }++  // Calculate the shift amount based on crop/border space available+  float u_crop = 1.0f - (u2 - u1);+  float v_crop = 1.0f - (v2 - v1);+  u1 += u_shift * u_crop;+  u2 += u_shift * u_crop;+  v1 += v_shift * v_crop;+  v2 += v_shift * v_crop;++  // Clamp to valid range [0, 1]+  if (u1 < 0.0f)+    u1 = 0.0f;+  if (u2 > 1.0f)+    u2 = 1.0f;+  if (v1 < 0.0f)+    v1 = 0.0f;+  if (v2 > 1.0f)+    v2 = 1.0f;+   // Set texture coordinates for quad (bottom-left, bottom-right, top-right,   // top-left)   tex_coords[0] = u1;@@ -688,11 +755,11 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {   // Update VBO only if needed. E.g, image changed, scale mode changed, or first   // render   if (output->vbo_dirty) {-    // Calculate texture coordinates based on scaling mode+    // Calculate texture coordinates based on scaling mode and anchor     float tex_coords[8];-    calculate_texture_coords(output->scale_mode, output->image->width,-                             output->image->height, output->width,-                             output->height, tex_coords);+    calculate_texture_coords(output->scale_mode, output->anchor,+                             output->image->width, output->image->height,+                             output->width, output->height, tex_coords);      // Create dynamic vertex data with calculated texture coordinates     float dynamic_vertices[] = {