diff --git a/install_files/com.sidevesh.Luminance.gschema.xml.in b/install_files/com.sidevesh.Luminance.gschema.xml.in
index 8ef070a..247275a 100644
--- a/install_files/com.sidevesh.Luminance.gschema.xml.in
+++ b/install_files/com.sidevesh.Luminance.gschema.xml.in
@@ -6,10 +6,25 @@
Whether to link display brightness adjustments
When true, changes to the brightness of one display will also affect the brightness of other displays connected to the same computer.
+
+ false
+ Whether to proportionally link display brightness adjustments
+ When true, changes to the brightness of one display will proportionally affect other displays based on each display's min/max range. Mutually exclusive with is-brightness-linked.
+
false
Whether to hide the internal display when the lid is closed
When true, the internal display will be hidden when the lid is closed. This is useful for laptops that are connected to external displays.
+
+ {}
+ Maximum brightness percentage per display
+ A dictionary mapping display names to their maximum brightness percentage (0-100). When set, the brightness slider for that display will be capped at the specified value instead of 100%.
+
+
+ {}
+ Minimum brightness percentage per display
+ A dictionary mapping display names to their minimum brightness percentage (0-100). When set, the brightness slider for that display will start at the specified value instead of 0%.
+
diff --git a/src/constants/main.c b/src/constants/main.c
index 3157bed..b0466f3 100644
--- a/src/constants/main.c
+++ b/src/constants/main.c
@@ -14,6 +14,18 @@
#define IS_BRIGHTNESS_LINKED_GSETTINGS_KEY "is-brightness-linked"
#endif
+#ifndef IS_BRIGHTNESS_PROPORTIONALLY_LINKED_GSETTINGS_KEY
+#define IS_BRIGHTNESS_PROPORTIONALLY_LINKED_GSETTINGS_KEY "is-brightness-proportionally-linked"
+#endif
+
#ifndef SHOULD_HIDE_INTERNAL_IF_LID_CLOSED_GSETTINGS_KEY
#define SHOULD_HIDE_INTERNAL_IF_LID_CLOSED_GSETTINGS_KEY "should-hide-internal-if-lid-closed"
#endif
+
+#ifndef MAX_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY
+#define MAX_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY "max-brightness-per-display"
+#endif
+
+#ifndef MIN_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY
+#define MIN_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY "min-brightness-per-display"
+#endif
diff --git a/src/states/brightness_range.c b/src/states/brightness_range.c
new file mode 100644
index 0000000..26bbcae
--- /dev/null
+++ b/src/states/brightness_range.c
@@ -0,0 +1,93 @@
+#include
+
+#include "../constants/main.c"
+
+#ifndef BRIGHTNESS_RANGE_STATE
+#define BRIGHTNESS_RANGE_STATE
+
+// --- Generic helper for per-display brightness limits (min or max) ---
+
+static GVariant* _get_brightness_dict(const gchar *gsettings_key) {
+ GSettings *settings = g_settings_new(APP_INFO_PACKAGE_NAME);
+ GVariant *dict = g_settings_get_value(settings, gsettings_key);
+ g_object_unref(settings);
+ return dict;
+}
+
+static void _set_brightness_dict(const gchar *gsettings_key, const gchar *display_name, gint percentage) {
+ GSettings *settings = g_settings_new(APP_INFO_PACKAGE_NAME);
+ GVariant *dict = g_settings_get_value(settings, gsettings_key);
+
+ GVariantBuilder builder;
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{si}"));
+
+ GVariantIter iter;
+ gchar *key;
+ gint32 val;
+ g_variant_iter_init(&iter, dict);
+ while (g_variant_iter_loop(&iter, "{&si}", &key, &val)) {
+ if (g_strcmp0(key, display_name) != 0) {
+ g_variant_builder_add(&builder, "{&si}", key, val);
+ }
+ }
+
+ g_variant_builder_add(&builder, "{&si}", display_name, percentage);
+
+ GVariant *new_dict = g_variant_builder_end(&builder);
+ g_settings_set_value(settings, gsettings_key, new_dict);
+
+ g_variant_unref(dict);
+ g_object_unref(settings);
+}
+
+static gdouble _get_brightness_from_dict(GVariant *dict, const gchar *display_name, gdouble default_value) {
+ gint32 int_val = 0;
+ if (g_variant_lookup(dict, display_name, "i", &int_val)) {
+ if (int_val >= 0 && int_val <= 100) {
+ return (gdouble)int_val;
+ }
+ }
+ return default_value;
+}
+
+// --- Max brightness ---
+
+GVariant *_max_brightness_cache = NULL;
+
+gdouble get_max_brightness_percentage(const gchar *display_name) {
+ if (_max_brightness_cache == NULL) {
+ _max_brightness_cache = _get_brightness_dict(MAX_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY);
+ }
+ return _get_brightness_from_dict(_max_brightness_cache, display_name, 100.0);
+}
+
+void set_max_brightness_percentage(const gchar *display_name, gint percentage) {
+ if (_max_brightness_cache != NULL) {
+ g_variant_unref(_max_brightness_cache);
+ _max_brightness_cache = NULL;
+ }
+ _set_brightness_dict(MAX_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY, display_name, percentage);
+ _max_brightness_cache = _get_brightness_dict(MAX_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY);
+}
+
+// --- Min brightness ---
+
+GVariant *_min_brightness_cache = NULL;
+
+gdouble get_min_brightness_percentage(const gchar *display_name) {
+ if (_min_brightness_cache == NULL) {
+ _min_brightness_cache = _get_brightness_dict(MIN_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY);
+ }
+ return _get_brightness_from_dict(_min_brightness_cache, display_name, 0.0);
+}
+
+void set_min_brightness_percentage(const gchar *display_name, gint percentage) {
+ if (_min_brightness_cache != NULL) {
+ g_variant_unref(_min_brightness_cache);
+ _min_brightness_cache = NULL;
+ }
+ _set_brightness_dict(MIN_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY, display_name, percentage);
+ _min_brightness_cache = _get_brightness_dict(MIN_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY);
+}
+
+#endif
diff --git a/src/states/is_brightness_linked.c b/src/states/is_brightness_linked.c
index 0ff58aa..23fc629 100644
--- a/src/states/is_brightness_linked.c
+++ b/src/states/is_brightness_linked.c
@@ -19,4 +19,18 @@ gboolean get_is_brightness_linked() {
return value;
}
+void set_is_brightness_proportionally_linked(gboolean value) {
+ GSettings *settings = g_settings_new(APP_INFO_PACKAGE_NAME);
+ g_settings_set_boolean(settings, IS_BRIGHTNESS_PROPORTIONALLY_LINKED_GSETTINGS_KEY, value);
+ g_object_unref(settings);
+}
+
+gboolean get_is_brightness_proportionally_linked() {
+ GSettings *settings = g_settings_new(APP_INFO_PACKAGE_NAME);
+ gboolean value = g_settings_get_boolean(settings, IS_BRIGHTNESS_PROPORTIONALLY_LINKED_GSETTINGS_KEY);
+ g_object_unref(settings);
+
+ return value;
+}
+
#endif
diff --git a/src/states/max_brightness.c b/src/states/max_brightness.c
new file mode 100644
index 0000000..905c50f
--- /dev/null
+++ b/src/states/max_brightness.c
@@ -0,0 +1,74 @@
+#include
+
+#include "../constants/main.c"
+
+#ifndef MAX_BRIGHTNESS_STATE
+#define MAX_BRIGHTNESS_STATE
+
+// Cache the GVariant dict so we don't re-read on every slider movement
+GVariant *_max_brightness_cache = NULL;
+
+static GVariant* _get_max_brightness_dict() {
+ GSettings *settings = g_settings_new(APP_INFO_PACKAGE_NAME);
+ GVariant *dict = g_settings_get_value(settings, MAX_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY);
+ g_object_unref(settings);
+ return dict;
+}
+
+static void _invalidate_cache() {
+ if (_max_brightness_cache != NULL) {
+ g_variant_unref(_max_brightness_cache);
+ _max_brightness_cache = NULL;
+ }
+}
+
+gdouble get_max_brightness_percentage(const gchar *display_name) {
+ if (_max_brightness_cache == NULL) {
+ _max_brightness_cache = _get_max_brightness_dict();
+ }
+
+ gdouble value = 100.0;
+
+ gint32 int_val = 0;
+ if (g_variant_lookup(_max_brightness_cache, display_name, "i", &int_val)) {
+ if (int_val > 0 && int_val <= 100) {
+ value = (gdouble)int_val;
+ }
+ }
+
+ return value;
+}
+
+void set_max_brightness_percentage(const gchar *display_name, gint percentage) {
+ _invalidate_cache();
+
+ GSettings *settings = g_settings_new(APP_INFO_PACKAGE_NAME);
+ GVariant *dict = g_settings_get_value(settings, MAX_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY);
+
+ GVariantBuilder builder;
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{si}"));
+
+ // Copy existing entries, skipping the one we're updating
+ GVariantIter iter;
+ gchar *key;
+ gint32 val;
+ g_variant_iter_init(&iter, dict);
+ while (g_variant_iter_loop(&iter, "{&si}", &key, &val)) {
+ if (g_strcmp0(key, display_name) != 0) {
+ g_variant_builder_add(&builder, "{&si}", key, val);
+ }
+ }
+
+ // Add the new/updated entry
+ g_variant_builder_add(&builder, "{&si}", display_name, percentage);
+
+ GVariant *new_dict = g_variant_builder_end(&builder);
+ g_settings_set_value(settings, MAX_BRIGHTNESS_PER_DISPLAY_GSETTINGS_KEY, new_dict);
+
+ g_variant_unref(dict);
+ g_object_unref(settings);
+
+ _max_brightness_cache = _get_max_brightness_dict();
+}
+
+#endif
diff --git a/src/ui/components/display_brightness_scale.c b/src/ui/components/display_brightness_scale.c
index 1f06eb2..e350004 100644
--- a/src/ui/components/display_brightness_scale.c
+++ b/src/ui/components/display_brightness_scale.c
@@ -5,11 +5,11 @@
static char* format_brightness_value(GtkScale *scale, gdouble value, gpointer data) {
(void)scale;
(void)data;
- return g_strdup_printf(" %d%%", (int)value);
+ return g_strdup_printf("%3d%%", (int)value);
}
-GtkWidget* get_display_brightness_scale(gdouble last_value, gdouble max_value) {
- GtkWidget *scale = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 0, max_value, 1);
+GtkWidget* get_display_brightness_scale(gdouble last_value, gdouble min_value, gdouble max_value) {
+ GtkWidget *scale = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, min_value, max_value, 1);
gtk_scale_set_draw_value(GTK_SCALE(scale), TRUE);
gtk_scale_set_value_pos(GTK_SCALE(scale), GTK_POS_RIGHT);
diff --git a/src/ui/components/link_brightness_check_button.c b/src/ui/components/link_brightness_check_button.c
index ddb2fcd..789761a 100644
--- a/src/ui/components/link_brightness_check_button.c
+++ b/src/ui/components/link_brightness_check_button.c
@@ -2,7 +2,7 @@
#include "../constants/main.c"
GtkWidget* get_link_brightness_checkbox(gboolean initial_value) {
- GtkWidget *link_brightness_checkbox = gtk_check_button_new_with_label("Sync brightness of all displays");
+ GtkWidget *link_brightness_checkbox = gtk_check_button_new_with_label("Sync brightness: same value");
gtk_check_button_set_active(GTK_CHECK_BUTTON(link_brightness_checkbox), initial_value);
gtk_widget_set_margin_start(link_brightness_checkbox, MARGIN_UNIT);
diff --git a/src/ui/components/range_label.c b/src/ui/components/range_label.c
new file mode 100644
index 0000000..c845abe
--- /dev/null
+++ b/src/ui/components/range_label.c
@@ -0,0 +1,171 @@
+#include
+#include "../constants/main.c"
+
+#ifndef RANGE_LABEL_COMPONENT
+#define RANGE_LABEL_COMPONENT
+
+typedef struct {
+ GtkWidget *box; // Contains either label or entry
+ GtkWidget *label; // The visible label (e.g., "60%")
+ GtkWidget *entry; // Hidden until clicked
+ gdouble current_value;
+ gdouble min_allowed;
+ gdouble max_allowed;
+ void (*on_value_changed)(gdouble new_value, gpointer user_data);
+ gpointer user_data;
+} RangeLabel;
+
+static void _range_label_show_label(RangeLabel *rl) {
+ gtk_widget_set_visible(rl->entry, FALSE);
+ gtk_widget_set_visible(rl->label, TRUE);
+}
+
+static void _range_label_show_entry(RangeLabel *rl) {
+ gchar text[16];
+ snprintf(text, sizeof(text), "%d", (int)rl->current_value);
+ gtk_editable_set_text(GTK_EDITABLE(rl->entry), text);
+ gtk_widget_set_visible(rl->label, FALSE);
+ gtk_widget_set_visible(rl->entry, TRUE);
+ gtk_widget_grab_focus(rl->entry);
+}
+
+static void _range_label_commit(RangeLabel *rl) {
+ const gchar *text = gtk_editable_get_text(GTK_EDITABLE(rl->entry));
+ gint new_value = atoi(text);
+
+ // Validate
+ if (new_value < (gint)rl->min_allowed || new_value > (gint)rl->max_allowed) {
+ // Revert
+ _range_label_show_label(rl);
+ return;
+ }
+
+ gdouble new_pct = (gdouble)new_value;
+ if (new_pct == rl->current_value) {
+ _range_label_show_label(rl);
+ return;
+ }
+
+ rl->current_value = new_pct;
+ gchar label_text[16];
+ snprintf(label_text, sizeof(label_text), "%d%%", (int)rl->current_value);
+ gtk_label_set_text(GTK_LABEL(rl->label), label_text);
+
+ _range_label_show_label(rl);
+
+ if (rl->on_value_changed != NULL) {
+ rl->on_value_changed(new_pct, rl->user_data);
+ }
+}
+
+static void _on_range_label_entry_activate(GtkEntry *entry, gpointer user_data) {
+ (void)entry;
+ RangeLabel *rl = (RangeLabel *)user_data;
+ _range_label_commit(rl);
+}
+
+static gboolean _on_range_label_entry_focus_out(GtkEventControllerFocus *controller, gpointer user_data) {
+ (void)controller;
+ RangeLabel *rl = (RangeLabel *)user_data;
+ _range_label_commit(rl);
+ return FALSE;
+}
+
+static gboolean _on_range_label_key_pressed(GtkEventControllerKey *controller, guint keyval, guint keycode, GdkModifierType state, gpointer user_data) {
+ (void)controller;
+ (void)keycode;
+ (void)state;
+ RangeLabel *rl = (RangeLabel *)user_data;
+ if (keyval == GDK_KEY_Escape) {
+ _range_label_show_label(rl);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void _on_range_label_clicked(GtkGestureClick *gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) {
+ (void)gesture;
+ (void)n_press;
+ (void)x;
+ (void)y;
+ RangeLabel *rl = (RangeLabel *)user_data;
+ _range_label_show_entry(rl);
+}
+
+GtkWidget* get_range_label(gdouble value, gdouble min_allowed, gdouble max_allowed, void (*on_value_changed)(gdouble, gpointer), gpointer user_data) {
+ RangeLabel *rl = malloc(sizeof(RangeLabel));
+ rl->current_value = value;
+ rl->min_allowed = min_allowed;
+ rl->max_allowed = max_allowed;
+ rl->on_value_changed = on_value_changed;
+ rl->user_data = user_data;
+
+ rl->box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_valign(rl->box, GTK_ALIGN_CENTER);
+
+ gchar label_text[16];
+ snprintf(label_text, sizeof(label_text), "%d%%", (int)value);
+ rl->label = gtk_label_new(label_text);
+ gtk_widget_add_css_class(rl->label, "dim-label");
+ gtk_widget_set_visible(rl->label, TRUE);
+ gtk_label_set_width_chars(GTK_LABEL(rl->label), 4);
+ gtk_label_set_max_width_chars(GTK_LABEL(rl->label), 4);
+
+ rl->entry = gtk_entry_new();
+ gtk_editable_set_width_chars(GTK_EDITABLE(rl->entry), 4);
+ gtk_entry_set_max_length(GTK_ENTRY(rl->entry), 3);
+ gtk_widget_set_visible(rl->entry, FALSE);
+
+ // Style the entry to be minimal
+ GtkCssProvider *css = gtk_css_provider_new();
+ gtk_css_provider_load_from_string(css,
+ ".dim-label { font-size: 0.8em; opacity: 0.6; padding: 2px 4px; }"
+ ".dim-label:hover { opacity: 1.0; text-decoration: underline; }");
+ gtk_style_context_add_provider_for_display(gdk_display_get_default(), GTK_STYLE_PROVIDER(css), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ gtk_box_append(GTK_BOX(rl->box), rl->label);
+ gtk_box_append(GTK_BOX(rl->box), rl->entry);
+
+ // Store pointer so update_range_label can find it
+ g_object_set_data(G_OBJECT(rl->box), "range-label", rl);
+
+ // Click on label to edit
+ GtkGesture *click_gesture = gtk_gesture_click_new();
+ g_signal_connect(click_gesture, "pressed", G_CALLBACK(_on_range_label_clicked), rl);
+ gtk_widget_add_controller(rl->label, GTK_EVENT_CONTROLLER(click_gesture));
+
+ // Entry signals
+ g_signal_connect(rl->entry, "activate", G_CALLBACK(_on_range_label_entry_activate), rl);
+
+ // Focus-out on entry
+ GtkEventController *focus_controller = gtk_event_controller_focus_new();
+ g_signal_connect(focus_controller, "leave", G_CALLBACK(_on_range_label_entry_focus_out), rl);
+ gtk_widget_add_controller(rl->entry, focus_controller);
+
+ // Escape key on entry
+ GtkEventController *key_controller = gtk_event_controller_key_new();
+ g_signal_connect(key_controller, "key-pressed", G_CALLBACK(_on_range_label_key_pressed), rl);
+ gtk_widget_add_controller(rl->entry, key_controller);
+
+ return rl->box;
+}
+
+// Update the allowed range and current value (e.g., when min/max changes)
+void update_range_label(GtkWidget *range_label_box, gdouble new_value, gdouble new_min_allowed, gdouble new_max_allowed) {
+ // Find the RangeLabel by walking the widget hierarchy — we store it as qdata
+ RangeLabel *rl = (RangeLabel *)g_object_get_data(G_OBJECT(range_label_box), "range-label");
+ if (rl == NULL) return;
+
+ rl->min_allowed = new_min_allowed;
+ rl->max_allowed = new_max_allowed;
+
+ // Clamp current value to new range
+ if (rl->current_value != new_value) {
+ rl->current_value = new_value;
+ gchar label_text[16];
+ snprintf(label_text, sizeof(label_text), "%d%%", (int)rl->current_value);
+ gtk_label_set_text(GTK_LABEL(rl->label), label_text);
+ }
+}
+
+#endif
diff --git a/src/ui/screens/show_displays.c b/src/ui/screens/show_displays.c
index fd61e52..8fcf1f5 100644
--- a/src/ui/screens/show_displays.c
+++ b/src/ui/screens/show_displays.c
@@ -1,7 +1,9 @@
#include
#include
#include "../../states/displays.c"
+#include "../../states/brightness_range.c"
#include "../components/flatpak_setup_dialog.c"
+#include "../components/range_label.c"
#ifndef BRIGHTNESS_DEBOUNCE_DELAY_MS
#define BRIGHTNESS_DEBOUNCE_DELAY_MS 300
@@ -11,17 +13,48 @@ typedef struct display_section {
GtkWidget *icon;
GtkWidget *label;
GtkWidget *scale;
+ GtkWidget *scale_row; // hbox containing min_label + scale + max_label
+ GtkWidget *min_label;
+ GtkWidget *max_label;
GtkWidget *separator_left_column;
GtkWidget *separator_right_column;
guint display_index;
+ gdouble min_percentage;
+ gdouble max_percentage;
+ char *display_name;
} display_section;
display_section **_display_sections;
guint _display_sections_count = 0;
+gboolean _show_brightness_range = FALSE;
+static GtkWidget *_sync_exact_checkbox = NULL;
+static GtkWidget *_sync_proportional_checkbox = NULL;
static guint *_pending_brightness_timeout_ids = NULL;
static gdouble *_pending_brightness_values = NULL;
+static void _rebuild_slider_for_display(guint index);
+void _link_proportional_brightness(GtkCheckButton *checkbox);
+
+// Compute proportional brightness for a target display given a source display's value
+static gdouble _proportional_brightness(guint source_section, gdouble source_value, guint target_section) {
+ gdouble src_min = _display_sections[source_section]->min_percentage;
+ gdouble src_max = _display_sections[source_section]->max_percentage;
+ gdouble tgt_min = _display_sections[target_section]->min_percentage;
+ gdouble tgt_max = _display_sections[target_section]->max_percentage;
+
+ gdouble src_range = src_max - src_min;
+ if (src_range == 0) return tgt_min; // source is fixed, just use target min
+
+ gdouble position = (source_value - src_min) / src_range; // 0.0 to 1.0
+ return tgt_min + position * (tgt_max - tgt_min);
+}
+
+// Returns true if any sync mode is active
+static gboolean _is_any_sync_active() {
+ return get_is_brightness_linked() || get_is_brightness_proportionally_linked();
+}
+
static gboolean _apply_debounced_brightness(gpointer user_data) {
guint index = GPOINTER_TO_UINT(user_data);
if (index >= _display_sections_count || _pending_brightness_values == NULL) {
@@ -31,10 +64,19 @@ static gboolean _apply_debounced_brightness(gpointer user_data) {
set_display_brightness_percentage(index, brightness, FALSE);
- if (get_is_brightness_linked()) {
+ gboolean exact_sync = get_is_brightness_linked();
+ gboolean prop_sync = get_is_brightness_proportionally_linked();
+
+ if (exact_sync || prop_sync) {
for (guint i = 0; i < _display_sections_count; i++) {
if (_display_sections[i]->display_index == index) continue;
- set_display_brightness_percentage(_display_sections[i]->display_index, brightness, FALSE);
+ gdouble linked_brightness;
+ if (prop_sync) {
+ linked_brightness = _proportional_brightness(index, brightness, i);
+ } else {
+ linked_brightness = CLAMP(brightness, _display_sections[i]->min_percentage, _display_sections[i]->max_percentage);
+ }
+ set_display_brightness_percentage(_display_sections[i]->display_index, linked_brightness, FALSE);
}
}
@@ -49,7 +91,9 @@ void _update_display_brightness(GtkRange *range, guint data) {
guint index_of_display_section = GPOINTER_TO_UINT(data);
gdouble new_value = gtk_range_get_value(range);
- new_value = CLAMP(new_value, 0.0, 100.0);
+ gdouble min_pct = _display_sections[index_of_display_section]->min_percentage;
+ gdouble max_pct = _display_sections[index_of_display_section]->max_percentage;
+ new_value = CLAMP(new_value, min_pct, max_pct);
if (_pending_brightness_timeout_ids[index_of_display_section] != 0) {
g_source_remove(_pending_brightness_timeout_ids[index_of_display_section]);
@@ -66,16 +110,21 @@ void _update_display_brightness(GtkRange *range, guint data) {
_pending_brightness_timeout_ids[index_of_display_section] = new_timeout_id;
}
- if (get_is_brightness_linked()) {
+ if (_is_any_sync_active()) {
+ gboolean prop_sync = get_is_brightness_proportionally_linked();
for (guint index = 0; index < _display_sections_count; index++) {
if (_display_sections[index]->display_index == index_of_display_section) {
continue;
}
GtkRange *linked_range = GTK_RANGE(_display_sections[index]->scale);
- // Block signal handler to prevent recursive calls and redundant debounce timers
- // when updating linked sliders programmatically.
g_signal_handlers_block_by_func(linked_range, (gpointer)_update_display_brightness, GUINT_TO_POINTER(_display_sections[index]->display_index));
- gtk_range_set_value(linked_range, new_value);
+ gdouble linked_value;
+ if (prop_sync) {
+ linked_value = _proportional_brightness(index_of_display_section, new_value, index);
+ } else {
+ linked_value = CLAMP(new_value, _display_sections[index]->min_percentage, _display_sections[index]->max_percentage);
+ }
+ gtk_range_set_value(linked_range, linked_value);
g_signal_handlers_unblock_by_func(linked_range, (gpointer)_update_display_brightness, GUINT_TO_POINTER(_display_sections[index]->display_index));
}
}
@@ -83,13 +132,25 @@ void _update_display_brightness(GtkRange *range, guint data) {
void _link_brightness(GtkCheckButton *link_brightness_checkbox) {
gboolean is_brightness_linked = gtk_check_button_get_active(link_brightness_checkbox);
- gdouble max_scale_percentage = 0;
set_is_brightness_linked(is_brightness_linked);
+
+ // Mutual exclusion: uncheck proportional if exact is checked
+ if (is_brightness_linked) {
+ set_is_brightness_proportionally_linked(FALSE);
+ if (_sync_proportional_checkbox != NULL) {
+ g_signal_handlers_block_by_func(_sync_proportional_checkbox, (gpointer)_link_proportional_brightness, NULL);
+ gtk_check_button_set_active(GTK_CHECK_BUTTON(_sync_proportional_checkbox), FALSE);
+ g_signal_handlers_unblock_by_func(_sync_proportional_checkbox, (gpointer)_link_proportional_brightness, NULL);
+ }
+ }
+
if (!is_brightness_linked) {
return;
}
+ // Sync all displays to the highest value
+ gdouble max_scale_percentage = 0;
for (guint index = 0; index < _display_sections_count; index++) {
guint percentage_value = gtk_range_get_value(GTK_RANGE(_display_sections[index]->scale));
max_scale_percentage = percentage_value > max_scale_percentage ? percentage_value : max_scale_percentage;
@@ -97,15 +158,124 @@ void _link_brightness(GtkCheckButton *link_brightness_checkbox) {
for (guint index = 0; index < _display_sections_count; index++) {
GtkRange *range = GTK_RANGE(_display_sections[index]->scale);
- // Block signal handler to verify we simply update the UI and apply brightness immediately
- // without triggering the debounce mechanism.
g_signal_handlers_block_by_func(range, (gpointer)_update_display_brightness, GUINT_TO_POINTER(_display_sections[index]->display_index));
- gtk_range_set_value(range, max_scale_percentage);
+ gdouble capped = CLAMP(max_scale_percentage, _display_sections[index]->min_percentage, _display_sections[index]->max_percentage);
+ gtk_range_set_value(range, capped);
+ g_signal_handlers_unblock_by_func(range, (gpointer)_update_display_brightness, GUINT_TO_POINTER(_display_sections[index]->display_index));
+ set_display_brightness_percentage(_display_sections[index]->display_index, capped, FALSE);
+ }
+}
+
+void _link_proportional_brightness(GtkCheckButton *checkbox) {
+ gboolean is_prop_linked = gtk_check_button_get_active(checkbox);
+
+ set_is_brightness_proportionally_linked(is_prop_linked);
+
+ // Mutual exclusion: uncheck exact if proportional is checked
+ if (is_prop_linked) {
+ set_is_brightness_linked(FALSE);
+ if (_sync_exact_checkbox != NULL) {
+ g_signal_handlers_block_by_func(_sync_exact_checkbox, (gpointer)_link_brightness, NULL);
+ gtk_check_button_set_active(GTK_CHECK_BUTTON(_sync_exact_checkbox), FALSE);
+ g_signal_handlers_unblock_by_func(_sync_exact_checkbox, (gpointer)_link_brightness, NULL);
+ }
+ }
+
+ if (!is_prop_linked) {
+ return;
+ }
+
+ // Find the display with the highest relative position in its range, then sync proportionally
+ // Use the first display as reference — sync all to its relative position
+ gdouble ref_value = gtk_range_get_value(GTK_RANGE(_display_sections[0]->scale));
+ guint ref_index = 0;
+
+ for (guint index = 0; index < _display_sections_count; index++) {
+ GtkRange *range = GTK_RANGE(_display_sections[index]->scale);
+ g_signal_handlers_block_by_func(range, (gpointer)_update_display_brightness, GUINT_TO_POINTER(_display_sections[index]->display_index));
+ gdouble prop_value = _proportional_brightness(ref_index, ref_value, index);
+ gtk_range_set_value(range, prop_value);
g_signal_handlers_unblock_by_func(range, (gpointer)_update_display_brightness, GUINT_TO_POINTER(_display_sections[index]->display_index));
- set_display_brightness_percentage(_display_sections[index]->display_index, max_scale_percentage, FALSE);
+ set_display_brightness_percentage(_display_sections[index]->display_index, prop_value, FALSE);
+ }
+}
+
+// --- Show/hide brightness range ---
+
+static void _toggle_brightness_range(GtkCheckButton *checkbox) {
+ _show_brightness_range = gtk_check_button_get_active(checkbox);
+ for (guint i = 0; i < _display_sections_count; i++) {
+ gtk_widget_set_visible(_display_sections[i]->min_label, _show_brightness_range);
+ gtk_widget_set_visible(_display_sections[i]->max_label, _show_brightness_range);
}
}
+// --- Min/Max label callbacks ---
+
+static void _on_min_value_changed(gdouble new_min, gpointer user_data) {
+ guint index = GPOINTER_TO_UINT(user_data);
+ if (index >= _display_sections_count) return;
+
+ // Enforce min <= max
+ gdouble current_max = _display_sections[index]->max_percentage;
+ if (new_min > current_max) new_min = current_max;
+ if (new_min < 0) new_min = 0;
+
+ _display_sections[index]->min_percentage = new_min;
+ set_min_brightness_percentage(_display_sections[index]->display_name, (gint)new_min);
+
+ // Rebuild slider — also recreates labels with updated ranges
+ _rebuild_slider_for_display(index);
+}
+
+static void _on_max_value_changed(gdouble new_max, gpointer user_data) {
+ guint index = GPOINTER_TO_UINT(user_data);
+ if (index >= _display_sections_count) return;
+
+ // Enforce max >= min
+ gdouble current_min = _display_sections[index]->min_percentage;
+ if (new_max < current_min) new_max = current_min;
+ if (new_max > 100) new_max = 100;
+
+ _display_sections[index]->max_percentage = new_max;
+ set_max_brightness_percentage(_display_sections[index]->display_name, (gint)new_max);
+
+ // Rebuild slider — also recreates labels with updated ranges
+ _rebuild_slider_for_display(index);
+}
+
+static void _rebuild_slider_for_display(guint index) {
+ if (index >= _display_sections_count || _display_sections[index]->scale == NULL) return;
+ GtkRange *range = GTK_RANGE(_display_sections[index]->scale);
+ gdouble min_pct = _display_sections[index]->min_percentage;
+ gdouble max_pct = _display_sections[index]->max_percentage;
+
+ if (min_pct == max_pct) {
+ // Fixed brightness: use 0-100 range, set value, disable
+ gtk_range_set_range(range, 0, 100);
+ gtk_range_set_value(range, min_pct);
+ gtk_widget_set_sensitive(_display_sections[index]->scale, FALSE);
+ set_display_brightness_percentage(index, min_pct, FALSE);
+ } else {
+ gtk_widget_set_sensitive(_display_sections[index]->scale, TRUE);
+ gtk_range_set_range(range, min_pct, max_pct);
+
+ gdouble current = gtk_range_get_value(range);
+ gdouble clamped = CLAMP(current, min_pct, max_pct);
+ gtk_range_set_value(range, clamped);
+
+ if (current != clamped) {
+ set_display_brightness_percentage(index, clamped, FALSE);
+ }
+ }
+
+ // Update min/max labels with new allowed ranges
+ if (_display_sections[index]->min_label != NULL)
+ update_range_label(_display_sections[index]->min_label, min_pct, 0, max_pct);
+ if (_display_sections[index]->max_label != NULL)
+ update_range_label(_display_sections[index]->max_label, max_pct, min_pct, 100);
+}
+
static void _cleanup_display_resources(GtkWidget *widget, gpointer data) {
(void)widget;
@@ -128,6 +298,9 @@ static void _cleanup_display_resources(GtkWidget *widget, gpointer data) {
if (_display_sections != NULL) {
for (guint i = 0; i < _display_sections_count; i++) {
if (_display_sections[i] != NULL) {
+ if (_display_sections[i]->display_name != NULL) {
+ free(_display_sections[i]->display_name);
+ }
free(_display_sections[i]);
}
}
@@ -154,18 +327,17 @@ GtkWidget* get_show_displays_screen() {
gtk_widget_add_css_class(banner, "banner");
gtk_widget_add_css_class(banner, "warning");
- GtkWidget *label = gtk_label_new("Missing permissions for built-in displays.");
- gtk_label_set_wrap(GTK_LABEL(label), TRUE);
- gtk_widget_set_hexpand(label, TRUE);
- gtk_widget_set_halign(label, GTK_ALIGN_START);
+ GtkWidget *banner_label = gtk_label_new("Missing permissions for built-in displays.");
+ gtk_label_set_wrap(GTK_LABEL(banner_label), TRUE);
+ gtk_widget_set_hexpand(banner_label, TRUE);
+ gtk_widget_set_halign(banner_label, GTK_ALIGN_START);
GtkWidget *button = gtk_button_new_with_label("Setup");
g_signal_connect(button, "clicked", G_CALLBACK(_on_setup_permissions_banner_button_clicked), NULL);
- gtk_box_append(GTK_BOX(banner), label);
+ gtk_box_append(GTK_BOX(banner), banner_label);
gtk_box_append(GTK_BOX(banner), button);
- // Add styling for banner
GtkCssProvider *provider = gtk_css_provider_new();
gtk_css_provider_load_from_string(provider,
".banner { padding: 12px; background-color: alpha(@warning_bg_color, 0.2); border-bottom: 1px solid alpha(@warning_fg_color, 0.2); }");
@@ -196,30 +368,104 @@ GtkWidget* get_show_displays_screen() {
for (guint index = 0; index < displays_count(); index++) {
display_section *display_section_instance = malloc(sizeof(display_section));
display_section_instance->display_index = index;
- display_section_instance->label = get_display_label(get_display_name(index));
- display_section_instance->scale = get_display_brightness_scale(get_display_brightness_percentage(index), 100.0);
+ char *display_name = get_display_name(index);
+ display_section_instance->display_name = strdup(display_name);
+ gdouble min_pct = get_min_brightness_percentage(display_name);
+ gdouble max_pct = get_max_brightness_percentage(display_name);
+ display_section_instance->min_percentage = min_pct;
+ display_section_instance->max_percentage = max_pct;
+ display_section_instance->label = get_display_label(display_name);
+ gdouble current_pct = get_display_brightness_percentage(index);
+ gdouble clamped_current = CLAMP(current_pct, min_pct, max_pct);
+ // If current brightness is outside the range, force it into range
+ if (current_pct < min_pct || current_pct > max_pct) {
+ set_display_brightness_percentage(index, clamped_current, FALSE);
+ }
+ // Create slider — for min==max (fixed), use 0-100 range but disable
+ gboolean is_fixed = (min_pct == max_pct);
+ GtkWidget *scale_widget;
+ if (is_fixed) {
+ scale_widget = get_display_brightness_scale(min_pct, 0, 100);
+ gtk_widget_set_sensitive(scale_widget, FALSE);
+ } else {
+ scale_widget = get_display_brightness_scale(clamped_current, min_pct, max_pct);
+ }
+ display_section_instance->scale = scale_widget;
g_signal_connect(display_section_instance->scale, "value-changed", G_CALLBACK(_update_display_brightness), GUINT_TO_POINTER(index));
+ // Create clickable min/max labels
+ // Min label: allowed range 0 to max
+ display_section_instance->min_label = get_range_label(
+ min_pct, 0, max_pct,
+ _on_min_value_changed, GUINT_TO_POINTER(index));
+ gtk_widget_set_visible(display_section_instance->min_label, FALSE);
+ // Max label: allowed range min to 100
+ display_section_instance->max_label = get_range_label(
+ max_pct, min_pct, 100,
+ _on_max_value_changed, GUINT_TO_POINTER(index));
+ gtk_widget_set_visible(display_section_instance->max_label, FALSE);
+
display_section_instance->separator_left_column = get_separator();
display_section_instance->separator_right_column = get_separator();
display_section_instance->icon = get_display_icon();
sections[index] = display_section_instance;
+ // Create a row: min_label + slider + max_label
+ GtkWidget *scale_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
+ gtk_widget_set_hexpand(scale_row, TRUE);
+ gtk_widget_set_valign(scale_row, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(scale_row), display_section_instance->min_label);
+ gtk_widget_set_valign(display_section_instance->min_label, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(scale_row), display_section_instance->scale);
+ gtk_widget_set_valign(display_section_instance->scale, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(scale_row), display_section_instance->max_label);
+ gtk_widget_set_valign(display_section_instance->max_label, GTK_ALIGN_CENTER);
+ display_section_instance->scale_row = scale_row;
+
if (sibling == NULL) {
gtk_grid_attach(GTK_GRID(grid), sections[index]->label, 1, 0, 1, 1);
} else {
gtk_grid_attach_next_to(GTK_GRID(grid), sections[index]->label, sibling->separator_right_column, GTK_POS_BOTTOM, 1, 1);
}
- gtk_grid_attach_next_to(GTK_GRID(grid), sections[index]->scale, sections[index]->label, GTK_POS_BOTTOM, 1, 1);
+ gtk_grid_attach_next_to(GTK_GRID(grid), scale_row, sections[index]->label, GTK_POS_BOTTOM, 1, 1);
gtk_grid_attach_next_to(GTK_GRID(grid), sections[index]->icon, sections[index]->label, GTK_POS_LEFT, 1, 2);
gtk_grid_attach_next_to(GTK_GRID(grid), sections[index]->separator_left_column, sections[index]->icon, GTK_POS_BOTTOM, 1, 1);
- gtk_grid_attach_next_to(GTK_GRID(grid), sections[index]->separator_right_column, sections[index]->scale, GTK_POS_BOTTOM, 1, 1);
+ gtk_grid_attach_next_to(GTK_GRID(grid), sections[index]->separator_right_column, scale_row, GTK_POS_BOTTOM, 1, 1);
sibling = sections[index];
}
link_brightness_checkbox = get_link_brightness_checkbox(get_is_brightness_linked());
+ gtk_widget_set_hexpand(link_brightness_checkbox, TRUE);
+ _sync_exact_checkbox = link_brightness_checkbox;
gtk_grid_attach_next_to(GTK_GRID(grid), link_brightness_checkbox, sibling->separator_right_column, GTK_POS_BOTTOM, 1, 1);
g_signal_connect(link_brightness_checkbox, "toggled", G_CALLBACK(_link_brightness), NULL);
+ // Proportional sync checkbox
+ GtkWidget *prop_sync_checkbox = gtk_check_button_new_with_label("Sync brightness: proportional");
+ gtk_widget_set_margin_start(prop_sync_checkbox, MARGIN_UNIT);
+ gtk_widget_set_margin_end(prop_sync_checkbox, MARGIN_UNIT);
+ gtk_widget_set_margin_top(prop_sync_checkbox, MARGIN_UNIT);
+ gtk_widget_set_margin_bottom(prop_sync_checkbox, MARGIN_UNIT);
+ gtk_widget_set_halign(prop_sync_checkbox, GTK_ALIGN_END);
+ gtk_widget_set_hexpand(prop_sync_checkbox, TRUE);
+ gtk_check_button_set_active(GTK_CHECK_BUTTON(prop_sync_checkbox), get_is_brightness_proportionally_linked());
+ _sync_proportional_checkbox = prop_sync_checkbox;
+ gtk_grid_attach_next_to(GTK_GRID(grid), prop_sync_checkbox, link_brightness_checkbox, GTK_POS_BOTTOM, 1, 1);
+ g_signal_connect(prop_sync_checkbox, "toggled", G_CALLBACK(_link_proportional_brightness), NULL);
+
+ // "Set brightness range" toggle — defaults to off, shows min/max labels when on
+ GtkWidget *range_toggle = gtk_check_button_new_with_label("Set brightness range");
+ gtk_widget_add_css_class(range_toggle, "flat");
+ _show_brightness_range = FALSE;
+ gtk_check_button_set_active(GTK_CHECK_BUTTON(range_toggle), FALSE);
+ gtk_widget_set_margin_start(range_toggle, MARGIN_UNIT);
+ gtk_widget_set_margin_end(range_toggle, MARGIN_UNIT);
+ gtk_widget_set_margin_top(range_toggle, MARGIN_UNIT);
+ gtk_widget_set_margin_bottom(range_toggle, MARGIN_UNIT);
+ gtk_widget_set_halign(range_toggle, GTK_ALIGN_END);
+ gtk_widget_set_hexpand(range_toggle, TRUE);
+ gtk_grid_attach_next_to(GTK_GRID(grid), range_toggle, prop_sync_checkbox, GTK_POS_BOTTOM, 1, 1);
+ g_signal_connect(range_toggle, "toggled", G_CALLBACK(_toggle_brightness_range), NULL);
+
return main_box;
}