Files
MontaukOS/programs/src/desktop/brightness.cpp
T

264 lines
10 KiB
C++

/*
* 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
}