feat: add laptop/display brightness modulation

This commit is contained in:
daniel committed 2026-10-02 12:14:40 +02:00
1 parent ef1e69ae83
commit 7979acc2eb
34 files changed
+1308 -78

No files matched your search

+3 -2
View File
@@ -20,7 +20,7 @@ extern "C" {
using namespace gui;
static constexpr int WIN_W = 760;
static constexpr int WIN_H = 560;
static constexpr int WIN_H = 582; // one grid row per capability
static constexpr int TAB_H = 36;
static constexpr int FOOTER_H = 44;
static constexpr int PAD = 18;
@@ -60,11 +60,12 @@ static const char* const kCapDescriptions[CAP_COUNT] = {
"Configure network interfaces, DHCP and Wi-Fi",
"Set the system clock",
"Manage user accounts and session identities",
"Change display modes and panel brightness",
"Change display modes",
"Manage hardware devices (Bluetooth, radio)",
"Read the kernel log",
"Write to 0:/os and 0:/apps (never part of \"all\")",
"Read and send input to other windows (never part of \"all\")",
"Set the display backlight",
};
// ==== Grant table model ====
+1 -1
View File
@@ -56,7 +56,7 @@ LDFLAGS := \
# ---- C++ source files ----
CORE_SRCS := main.cpp window.cpp panel.cpp wifi.cpp battery.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp consent.cpp font_data.cpp stb_truetype_impl.cpp
CORE_SRCS := main.cpp window.cpp panel.cpp wifi.cpp battery.cpp brightness.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp consent.cpp font_data.cpp stb_truetype_impl.cpp
APP_SRCS := $(sort $(shell find apps -name '*.cpp' -print))
SRCS := $(CORE_SRCS) $(APP_SRCS)
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
+263
View File
@@ -0,0 +1,263 @@
/*
* brightness.cpp
* Panel brightness control: the icon that appears when the active display
* has a controllable backlight (laptop panel or DDC/CI monitor), and the
* slider popout that drives it.
*
* Copyright (c) 2026 Daniel Hammer
*/
#include "desktop_internal.hpp"
#include <gui/mtk.hpp>
using namespace gui;
// ============================================================================
// Geometry
// ============================================================================
static constexpr int BRIGHT_POPUP_W = 220;
static constexpr int BRIGHT_SLIDER_INSET = 16;
static constexpr int BRIGHT_SLIDER_H = 8;
static constexpr int BRIGHT_KNOB_R = 8;
static constexpr int BRIGHT_STEP = 5; // [-]/[+] and one wheel notch
static constexpr int BRIGHT_MIN = 5; // keep the screen readable
static constexpr int BRIGHT_POLL_MS = 2000; // follow the Display app
static constexpr int BRIGHT_ABSENT_POLL_MS = 30000;
// A DDC/CI monitor needs ~50ms per command, so a drag is sampled rather than
// forwarding every mouse move; the release always sends the final level.
static constexpr int BRIGHT_DRAG_SEND_MS = 100;
static const char* DISPLAY_APP_PATH = "0:/apps/display/display.elf";
static int brightness_popup_x(const DesktopState* ds) {
int x = ds->brightness_icon_rect.x + ds->brightness_icon_rect.w - BRIGHT_POPUP_W;
return x < 4 ? 4 : x;
}
static int brightness_header_y(const DesktopState* ds) {
(void)ds;
return PANEL_HEIGHT + 2 + 12;
}
static Rect brightness_slider_rect(const DesktopState* ds) {
int y = brightness_header_y(ds) + system_font_height() + 18;
return {brightness_popup_x(ds) + BRIGHT_SLIDER_INSET, y,
BRIGHT_POPUP_W - 2 * BRIGHT_SLIDER_INSET, BRIGHT_SLIDER_H};
}
// [-] [+] row under the slider.
static Rect brightness_minus_rect(const DesktopState* ds) {
Rect s = brightness_slider_rect(ds);
return {brightness_popup_x(ds) + (BRIGHT_POPUP_W - (36 * 2 + 8)) / 2,
s.y + s.h + 16, 36, 24};
}
static Rect brightness_plus_rect(const DesktopState* ds) {
Rect m = brightness_minus_rect(ds);
return {m.x + m.w + 8, m.y, m.w, m.h};
}
static Rect brightness_settings_rect(const DesktopState* ds) {
Rect m = brightness_minus_rect(ds);
return {brightness_popup_x(ds) + 8, m.y + m.h + 12,
BRIGHT_POPUP_W - 16, system_font_height() + 10};
}
static Rect brightness_popup_rect(const DesktopState* ds) {
Rect settings = brightness_settings_rect(ds);
int y = PANEL_HEIGHT + 2;
return {brightness_popup_x(ds), y, BRIGHT_POPUP_W, settings.y + settings.h + 8 - y};
}
// ============================================================================
// Hardware access
// ============================================================================
static int clamp_level(int level) {
if (level < BRIGHT_MIN) return BRIGHT_MIN;
if (level > 100) return 100;
return level;
}
// Send `level` now. The kernel reports what the hardware ended up at, which
// for a panel with coarse steps is not always exactly what was asked.
static void brightness_apply(DesktopState* ds, int level) {
ds->brightness_pending = -1;
ds->brightness_last_send = montauk::get_milliseconds();
int result = montauk::display_brightness(level);
if (result >= 0 && !ds->brightness_dragging) ds->brightness_level = result;
}
// While dragging, the slider tracks the pointer immediately and the hardware
// catches up at most every BRIGHT_DRAG_SEND_MS.
static void brightness_request(DesktopState* ds, int level) {
level = clamp_level(level);
ds->brightness_level = level;
uint64_t now = montauk::get_milliseconds();
if (ds->brightness_dragging && now - ds->brightness_last_send < BRIGHT_DRAG_SEND_MS) {
ds->brightness_pending = level;
return;
}
brightness_apply(ds, level);
}
static int brightness_from_x(const DesktopState* ds, int mx) {
Rect s = brightness_slider_rect(ds);
int v = ((mx - s.x) * 100) / s.w;
return clamp_level(v);
}
// ============================================================================
// Startup and polling
// ============================================================================
void desktop_brightness_init(DesktopState* ds) {
ds->icon_brightness = svg_load("0:/os/icons/display-brightness-symbolic.svg",
16, 16, colors::PANEL_TEXT);
ds->brightness_popup_open = false;
ds->brightness_icon_rect = {0, 0, 0, 0};
ds->brightness_dragging = false;
ds->brightness_pending = -1;
ds->brightness_last_send = 0;
ds->brightness_last_poll = montauk::get_milliseconds();
int level = montauk::display_brightness(-1);
ds->brightness_present = level >= 0;
ds->brightness_level = level >= 0 ? level : 100;
}
bool desktop_brightness_poll(DesktopState* ds, uint64_t now) {
// Flush a drag sample that was held back by the send throttle.
if (ds->brightness_pending >= 0 && now - ds->brightness_last_send >= BRIGHT_DRAG_SEND_MS)
brightness_apply(ds, ds->brightness_pending);
if (ds->brightness_dragging) return false;
// An absent backlight is re-checked now and then: an external monitor
// can be switched to DDC/CI in its menu, and the kernel retries that.
uint64_t interval = ds->brightness_present ? BRIGHT_POLL_MS : BRIGHT_ABSENT_POLL_MS;
if (now - ds->brightness_last_poll < interval) return false;
ds->brightness_last_poll = now;
int level = montauk::display_brightness(-1);
bool present = level >= 0;
bool changed = present != ds->brightness_present;
ds->brightness_present = present;
if (!present) {
if (changed) ds->brightness_popup_open = false;
return changed;
}
if (level != ds->brightness_level) {
ds->brightness_level = level;
changed = true;
}
return changed;
}
bool desktop_brightness_dragging(const DesktopState* ds) {
return ds->brightness_dragging;
}
// ============================================================================
// Popout
// ============================================================================
static void draw_button(DesktopState* ds, const Rect& r, const char* label) {
Framebuffer& fb = ds->fb;
Color bg = r.contains(ds->mouse.x, ds->mouse.y) ? Color::from_rgb(0xD0, 0xD0, 0xD0)
: Color::from_rgb(0xE0, 0xE0, 0xE0);
fill_rounded_rect(fb, r.x, r.y, r.w, r.h, 6, bg);
int tw = text_width(label);
draw_text(fb, r.x + (r.w - tw) / 2, r.y + (r.h - system_font_height()) / 2,
label, colors::TEXT_COLOR);
}
void desktop_draw_brightness_popup(DesktopState* ds) {
Framebuffer& fb = ds->fb;
Rect popup = brightness_popup_rect(ds);
Color accent = ds->settings.accent_color;
draw_shadow(fb, popup.x, popup.y, popup.w, popup.h, 4, colors::SHADOW);
fill_rounded_rect(fb, popup.x, popup.y, popup.w, popup.h, 8, colors::MENU_BG);
draw_rect(fb, popup.x, popup.y, popup.w, popup.h, colors::BORDER);
int ty = brightness_header_y(ds);
draw_text(fb, popup.x + 14, ty, "Brightness", colors::TEXT_COLOR);
char value[8];
snprintf(value, sizeof(value), "%d%%", ds->brightness_level);
draw_text(fb, popup.x + popup.w - 14 - text_width(value), ty, value, accent);
Rect s = brightness_slider_rect(ds);
fill_rounded_rect(fb, s.x, s.y, s.w, s.h, 4, Color::from_rgb(0xDD, 0xDD, 0xDD));
int fill_w = (ds->brightness_level * s.w) / 100;
if (fill_w > 0) fill_rounded_rect(fb, s.x, s.y, fill_w, s.h, 4, accent);
int knob_cx = s.x + fill_w;
int knob_cy = s.y + s.h / 2;
fill_circle(fb, knob_cx, knob_cy, BRIGHT_KNOB_R, accent);
fill_circle(fb, knob_cx, knob_cy, BRIGHT_KNOB_R - 3, colors::WHITE);
draw_button(ds, brightness_minus_rect(ds), "-");
draw_button(ds, brightness_plus_rect(ds), "+");
Rect settings = brightness_settings_rect(ds);
if (settings.contains(ds->mouse.x, ds->mouse.y))
fill_rounded_rect(fb, settings.x, settings.y, settings.w, settings.h, 5,
Color::from_rgb(0xF0, 0xF3, 0xF6));
draw_text(fb, settings.x + 6, settings.y + (settings.h - system_font_height()) / 2,
"Display Settings...", colors::TEXT_COLOR);
}
// Returns true when the event was consumed.
bool desktop_brightness_handle_mouse(DesktopState* ds, int mx, int my, bool left_pressed,
bool left_held, bool left_released, int scroll) {
if (!ds->brightness_present) return false;
if (ds->brightness_dragging) {
if (left_held) {
brightness_request(ds, brightness_from_x(ds, mx));
return true;
}
if (left_released) {
ds->brightness_dragging = false;
brightness_apply(ds, ds->brightness_level);
return true;
}
}
// The wheel works over the panel icon too, without opening anything.
bool over_popup = ds->brightness_popup_open && brightness_popup_rect(ds).contains(mx, my);
if (scroll != 0 && (over_popup || ds->brightness_icon_rect.contains(mx, my))) {
brightness_request(ds, ds->brightness_level + scroll * BRIGHT_STEP);
return true;
}
if (!ds->brightness_popup_open || !left_pressed) return false;
if (!over_popup) {
if (!ds->brightness_icon_rect.contains(mx, my)) ds->brightness_popup_open = false;
return false;
}
Rect s = brightness_slider_rect(ds);
if (my >= s.y - 10 && my <= s.y + s.h + 10
&& mx >= s.x - BRIGHT_KNOB_R && mx <= s.x + s.w + BRIGHT_KNOB_R) {
ds->brightness_dragging = true;
brightness_request(ds, brightness_from_x(ds, mx));
return true;
}
if (brightness_minus_rect(ds).contains(mx, my)) {
brightness_request(ds, ds->brightness_level - BRIGHT_STEP);
return true;
}
if (brightness_plus_rect(ds).contains(mx, my)) {
brightness_request(ds, ds->brightness_level + BRIGHT_STEP);
return true;
}
if (brightness_settings_rect(ds).contains(mx, my)) {
ds->brightness_popup_open = false;
desktop_spawn_app(ds, DISPLAY_APP_PATH, nullptr);
return true;
}
return true; // inside the popup, on nothing in particular
}
+4
View File
@@ -273,6 +273,10 @@ void gui::desktop_compose(DesktopState* ds) {
desktop_draw_battery_popup(ds);
}
if (ds->brightness_popup_open) {
desktop_draw_brightness_popup(ds);
}
// Draw volume popup if open
if (ds->vol_popup_open) {
desktop_draw_vol_popup(ds);
+1
View File
@@ -68,6 +68,7 @@ void desktop_lock_screen(DesktopState* ds) {
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->vol_popup_open = false;
ds->brightness_popup_open = false;
// Cache display name for lock screen rendering
montauk::strncpy(ds->lock_display_name, ds->current_user, sizeof(ds->lock_display_name));
@@ -205,6 +205,15 @@ bool desktop_battery_poll(gui::DesktopState* ds, uint64_t now);
void desktop_draw_battery_popup(gui::DesktopState* ds);
bool desktop_battery_handle_mouse(gui::DesktopState* ds, int mx, int my);
// brightness.cpp
void desktop_brightness_init(gui::DesktopState* ds);
bool desktop_brightness_poll(gui::DesktopState* ds, uint64_t now);
bool desktop_brightness_dragging(const gui::DesktopState* ds);
void desktop_draw_brightness_popup(gui::DesktopState* ds);
bool desktop_brightness_handle_mouse(gui::DesktopState* ds, int mx, int my,
bool left_pressed, bool left_held,
bool left_released, int scroll);
// compose.cpp
void desktop_draw_lock_screen(gui::DesktopState* ds);
void desktop_mark_background_dirty(gui::DesktopState* ds);
+28
View File
@@ -187,6 +187,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false;
gui::mtk::context_menu_close(ds->ctx_menu);
ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->battery_popup_open = false;
@@ -518,11 +519,17 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
return; // click inside popup but not on any control
} else if (!ds->vol_icon_rect.contains(mx, my)) {
ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->vol_dragging = false;
}
}
}
// Brightness popup, a drag on its slider, or the wheel over its icon.
if (desktop_brightness_handle_mouse(ds, mx, my, left_pressed, left_held,
left_released, ev.scroll))
return;
// Wi-Fi popup. The passphrase dialog is an ordinary window and needs
// nothing here.
if (ds->wifi_popup_open) {
@@ -566,6 +573,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->wifi_popup_open = false;
ds->battery_popup_open = false;
ds->vol_popup_open = false;
ds->brightness_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu);
return;
}
@@ -583,6 +591,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->wifi_popup_open = false;
ds->battery_popup_open = false;
ds->vol_popup_open = false;
ds->brightness_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu);
return;
}
@@ -591,6 +600,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
if (ds->vol_icon_rect.w > 0 && ds->vol_icon_rect.contains(mx, my)) {
ds->vol_popup_open = !ds->vol_popup_open;
ds->vol_dragging = false;
ds->brightness_popup_open = false;
ds->app_menu_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
@@ -599,11 +609,25 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
return;
}
// Brightness icon
if (ds->brightness_present && ds->brightness_icon_rect.w > 0
&& ds->brightness_icon_rect.contains(mx, my)) {
ds->brightness_popup_open = !ds->brightness_popup_open;
ds->app_menu_open = false;
ds->vol_popup_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->battery_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu);
return;
}
// Network icon
if (ds->net_icon_rect.w > 0 && ds->net_icon_rect.contains(mx, my)) {
ds->net_popup_open = !ds->net_popup_open;
ds->app_menu_open = false;
ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->wifi_popup_open = false;
ds->battery_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu);
@@ -615,6 +639,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->wifi_popup_open = !ds->wifi_popup_open;
ds->app_menu_open = false;
ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->net_popup_open = false;
ds->battery_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu);
@@ -626,6 +651,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->battery_popup_open = !ds->battery_popup_open;
ds->app_menu_open = false;
ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu);
@@ -783,6 +809,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false;
gui::mtk::context_menu_close(ds->ctx_menu);
ds->vol_popup_open = false;
ds->brightness_popup_open = false;
}
// Forward continuous mouse events to focused window (hover, drag, release).
@@ -861,6 +888,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->wifi_popup_open = false;
ds->battery_popup_open = false;
ds->vol_popup_open = false;
ds->brightness_popup_open = false;
}
}
}
+1
View File
@@ -408,6 +408,7 @@ void desktop_open_launcher(DesktopState* ds) {
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->vol_dragging = false;
desktop_update_launcher_results(ds);
}
+3
View File
@@ -393,6 +393,7 @@ void gui::desktop_init(DesktopState* ds) {
desktop_refresh_netifs(ds);
desktop_wifi_init(ds);
desktop_battery_init(ds);
desktop_brightness_init(ds);
desktop_consent_init(ds);
ds->vol_popup_open = false;
@@ -445,6 +446,7 @@ static bool desktop_has_visible_dirty_window(const DesktopState* ds) {
static bool desktop_has_active_interaction(const DesktopState* ds) {
if (ds->vol_dragging) return true;
if (desktop_brightness_dragging(ds)) return true;
for (int i = 0; i < ds->window_count; i++) {
const Window& win = ds->windows[i];
@@ -521,6 +523,7 @@ static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
if (desktop_wifi_poll(ds, now)) changed = true;
if (desktop_battery_poll(ds, now)) changed = true;
if (desktop_brightness_poll(ds, now)) changed = true;
// Event-driven sync: the kernel mixer bumps a serial on every state
// change. Reading it is one cheap syscall; refresh only when the serial
+11
View File
@@ -173,6 +173,17 @@ void gui::desktop_draw_panel(DesktopState* ds) {
int icon_y = (PANEL_HEIGHT - 16) / 2;
int next_icon_x = vol_icon_x - 16 - 10;
// Brightness sits next to the speaker: the two slider controls together.
if (ds->brightness_present) {
ds->brightness_icon_rect = {next_icon_x, icon_y, 16, 16};
next_icon_x -= 16 + 10;
if (ds->icon_brightness.pixels)
fb.blit_alpha(ds->brightness_icon_rect.x, icon_y, ds->icon_brightness.width,
ds->icon_brightness.height, ds->icon_brightness.pixels);
} else {
ds->brightness_icon_rect = {0, 0, 0, 0};
}
if (ds->eth_present) {
ds->net_icon_rect = {next_icon_x, icon_y, 16, 16};
next_icon_x -= 16 + 10;
+1
View File
@@ -12,6 +12,7 @@
#include <montauk/heap.h>
#include <montauk/user.h>
#include <montauk/config.h>
#include <montauk/capabilities.h>
#include <gui/gui.hpp>
#include <gui/framebuffer.hpp>
#include <gui/draw.hpp>
+4
View File
@@ -52,6 +52,10 @@ void launch_session(LoginState* ls) {
caps.effective = montauk::user::is_admin(ls->username)
? montauk::abi::CAP_ADMIN_SESSION
: montauk::abi::CAP_STANDARD_SESSION;
// Clamp to what this login may delegate: a grant table from before a
// capability existed would otherwise make the kernel refuse the spawn
// outright, leaving no session at all rather than one without it.
caps.effective &= montauk::caps::self_delegable();
caps.delegable = caps.effective;
int pid;
+1
View File
@@ -30,6 +30,7 @@ static void maybe_run_setup_session() {
caps.effective = admin
? montauk::abi::CAP_ADMIN_SESSION
: montauk::abi::CAP_STANDARD_SESSION;
caps.effective &= montauk::caps::self_delegable();
// Matches launch_session(): a session delegates from what it holds, and
// the grant table decides which program receives which part of it.
caps.delegable = caps.effective;