{render,config}: allow specifying wallpaper anchor position

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Iee73e9d149e85d2c00eaba4be25d42bd6a6a6964
This commit is contained in:
raf 2026-04-13 14:25:47 +03:00
commit a82b986ac6
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 189 additions and 29 deletions

19
include/chroma.h vendored
View file

@ -44,6 +44,19 @@ typedef enum {
CHROMA_SCALE_CENTER = 3 // center image at original size CHROMA_SCALE_CENTER = 3 // center image at original size
} chroma_scale_mode_t; } 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 // Image filtering quality settings
typedef enum { typedef enum {
CHROMA_FILTER_NEAREST = 0, // nearest neighbor filtering (pixelated) CHROMA_FILTER_NEAREST = 0, // nearest neighbor filtering (pixelated)
@ -90,6 +103,7 @@ typedef struct {
// Configuration for this output // Configuration for this output
chroma_scale_mode_t scale_mode; chroma_scale_mode_t scale_mode;
chroma_filter_quality_t filter_quality; chroma_filter_quality_t filter_quality;
chroma_anchor_t anchor;
bool config_loaded; bool config_loaded;
// OpenGL resource cache // OpenGL resource cache
@ -108,6 +122,7 @@ typedef struct {
char image_path[MAX_PATH_LEN]; char image_path[MAX_PATH_LEN];
chroma_scale_mode_t scale_mode; chroma_scale_mode_t scale_mode;
chroma_filter_quality_t filter_quality; chroma_filter_quality_t filter_quality;
chroma_anchor_t anchor;
} chroma_config_mapping_t; } chroma_config_mapping_t;
// Application configuration // Application configuration
@ -120,6 +135,7 @@ typedef struct {
// Global scaling and filtering settings (used as defaults) // Global scaling and filtering settings (used as defaults)
chroma_scale_mode_t default_scale_mode; chroma_scale_mode_t default_scale_mode;
chroma_filter_quality_t default_filter_quality; chroma_filter_quality_t default_filter_quality;
chroma_anchor_t default_anchor;
// Image downsampling settings // Image downsampling settings
bool enable_downsampling; // enable automatic downsampling 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); const char *output_name);
int chroma_config_get_mapping_for_output( int chroma_config_get_mapping_for_output(
chroma_config_t *config, const char *output_name, 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); void chroma_config_print(const chroma_config_t *config);

View file

@ -6,8 +6,6 @@
#include "../include/chroma.h" #include "../include/chroma.h"
// Default configuration values
static char *trim_whitespace(char *str) { static char *trim_whitespace(char *str) {
char *end; 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 // Output-to-image mapping
static int add_output_mapping(chroma_config_t *config, const char *output_name, static int add_output_mapping(chroma_config_t *config, const char *output_name,
const char *image_path, const char *image_path,
chroma_scale_mode_t scale_mode, 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) { if (!config || !output_name || !image_path) {
return CHROMA_ERROR_INIT; 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); strcpy(mapping->image_path, image_path);
mapping->scale_mode = scale_mode; mapping->scale_mode = scale_mode;
mapping->filter_quality = filter_quality; mapping->filter_quality = filter_quality;
mapping->anchor = anchor;
config->mapping_count++; config->mapping_count++;
chroma_log("DEBUG", "Added mapping: %s -> %s (scale: %s, filter: %s)", chroma_log(
output_name, image_path, scale_mode_to_string(scale_mode), "DEBUG", "Added mapping: %s -> %s (scale: %s, filter: %s, anchor: %s)",
filter_quality_to_string(filter_quality)); 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)", chroma_log("TRACE", "Output mapping %d: '%s' -> '%s' (path length: %zu)",
config->mapping_count, output_name, image_path, path_len); config->mapping_count, output_name, image_path, path_len);
return CHROMA_OK; return CHROMA_OK;
@ -183,6 +243,7 @@ static void init_default_config(chroma_config_t *config) {
// Set default scaling and filtering // Set default scaling and filtering
config->default_scale_mode = CHROMA_SCALE_FILL; config->default_scale_mode = CHROMA_SCALE_FILL;
config->default_filter_quality = CHROMA_FILTER_LINEAR; config->default_filter_quality = CHROMA_FILTER_LINEAR;
config->default_anchor = CHROMA_ANCHOR_CENTER;
// Set default downsampling settings // Set default downsampling settings
config->enable_downsampling = true; // enable by default, performance etc. 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); config->enable_downsampling = parse_bool(value);
chroma_log("DEBUG", "Set downsampling: %s", chroma_log("DEBUG", "Set downsampling: %s",
config->enable_downsampling ? "enabled" : "disabled"); 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) { } else if (strcasecmp(key, "max_output_width") == 0) {
int width = atoi(value); int width = atoi(value);
if (width > 0 && width <= 16384) { // Reasonable limits 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); mapping->filter_quality = parse_filter_quality(value);
chroma_log("DEBUG", "Set filter quality for output %s: %s", output_name, chroma_log("DEBUG", "Set filter quality for output %s: %s", output_name,
filter_quality_to_string(mapping->filter_quality)); 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 { } else {
chroma_log("WARN", "Unknown output property: %s (line %d)", property, chroma_log("WARN", "Unknown output property: %s (line %d)", property,
line_number); 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, if (add_output_mapping(config, output_name, path_to_validate,
config->default_scale_mode, 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, chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name,
path_to_validate); path_to_validate);
if (expanded_path) { if (expanded_path) {
@ -518,12 +588,13 @@ const char *chroma_config_get_image_for_output(chroma_config_t *config,
return NULL; return NULL;
} }
// Get configuration mapping for output, including scale mode and filter // Get configuration mapping for output, including scale mode, filter
// quality // quality, and anchor
int chroma_config_get_mapping_for_output( int chroma_config_get_mapping_for_output(
chroma_config_t *config, const char *output_name, 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,
if (!config || !output_name || !scale_mode || !filter_quality) { chroma_anchor_t *anchor) {
if (!config || !output_name || !scale_mode || !filter_quality || !anchor) {
return CHROMA_ERROR_INIT; 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) { if (strcmp(config->mappings[i].output_name, output_name) == 0) {
*scale_mode = config->mappings[i].scale_mode; *scale_mode = config->mappings[i].scale_mode;
*filter_quality = config->mappings[i].filter_quality; *filter_quality = config->mappings[i].filter_quality;
*anchor = config->mappings[i].anchor;
chroma_log("DEBUG", 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), 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; return CHROMA_OK;
} }
} }
@ -543,14 +617,14 @@ int chroma_config_get_mapping_for_output(
// Return defaults if no specific mapping found // Return defaults if no specific mapping found
*scale_mode = config->default_scale_mode; *scale_mode = config->default_scale_mode;
*filter_quality = config->default_filter_quality; *filter_quality = config->default_filter_quality;
chroma_log("DEBUG", "Using defaults for output %s: scale=%s, filter=%s", *anchor = config->default_anchor;
output_name, scale_mode_to_string(*scale_mode), chroma_log(
filter_quality_to_string(*filter_quality)); "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; return CHROMA_OK;
} }
// Print current configuration for debugging // Print current configuration for debugging
void chroma_config_print(const chroma_config_t *config) { void chroma_config_print(const chroma_config_t *config) {
if (!config) { if (!config) {

View file

@ -112,19 +112,21 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
// Store old configuration values for comparison // Store old configuration values for comparison
chroma_scale_mode_t old_scale_mode = output->scale_mode; chroma_scale_mode_t old_scale_mode = output->scale_mode;
chroma_filter_quality_t old_filter_quality = output->filter_quality; chroma_filter_quality_t old_filter_quality = output->filter_quality;
chroma_anchor_t old_anchor = output->anchor;
bool had_config = output->config_loaded; 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( if (chroma_config_get_mapping_for_output(
&state->config, output->name ? output->name : "unknown", &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; output->config_loaded = true;
chroma_log("DEBUG", "Loaded config for output %u: scale=%d, filter=%d", chroma_log("DEBUG", "Loaded config for output %u: scale=%d, filter=%d, anchor=%d",
output->id, output->scale_mode, output->filter_quality); output->id, output->scale_mode, output->filter_quality, output->anchor);
// Check if configuration changed and invalidate texture if needed // Check if configuration changed and invalidate texture if needed
if (had_config && (old_scale_mode != output->scale_mode || 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); chroma_output_invalidate_texture(output);
output->vbo_dirty = true; // VBO needs update for new scale mode output->vbo_dirty = true; // VBO needs update for new scale mode
chroma_log("DEBUG", chroma_log("DEBUG",

View file

@ -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, static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
chroma_anchor_t anchor,
int image_width, int image_height, int image_width, int image_height,
int output_width, int output_height, int output_width, int output_height,
float tex_coords[8]) { float tex_coords[8]) {
@ -142,6 +143,72 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
break; 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, // Set texture coordinates for quad (bottom-left, bottom-right, top-right,
// top-left) // top-left)
tex_coords[0] = u1; 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 // Update VBO only if needed. E.g, image changed, scale mode changed, or first
// render // render
if (output->vbo_dirty) { if (output->vbo_dirty) {
// Calculate texture coordinates based on scaling mode // Calculate texture coordinates based on scaling mode and anchor
float tex_coords[8]; float tex_coords[8];
calculate_texture_coords(output->scale_mode, output->image->width, calculate_texture_coords(output->scale_mode, output->anchor,
output->image->height, output->width, output->image->width, output->image->height,
output->height, tex_coords); output->width, output->height, tex_coords);
// Create dynamic vertex data with calculated texture coordinates // Create dynamic vertex data with calculated texture coordinates
float dynamic_vertices[] = { float dynamic_vertices[] = {