264 lines
10 KiB
C++
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
|
|
}
|