239 lines
8.6 KiB
C++
239 lines
8.6 KiB
C++
/*
|
|
* battery.cpp
|
|
* Panel battery indicator: the icon that only appears on a machine with a
|
|
* battery, and the popout that puts a number on it.
|
|
*
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#include "desktop_internal.hpp"
|
|
#include <gui/mtk.hpp>
|
|
|
|
using namespace gui;
|
|
|
|
// ============================================================================
|
|
// Geometry
|
|
// ============================================================================
|
|
|
|
static constexpr int BATT_POPUP_W = 210;
|
|
static constexpr int BATT_POLL_MS = 5000; // the kernel caches for 2s anyway
|
|
|
|
static int battery_popup_x(const DesktopState* ds) {
|
|
int x = ds->battery_icon_rect.x + ds->battery_icon_rect.w - BATT_POPUP_W;
|
|
return x < 4 ? 4 : x;
|
|
}
|
|
|
|
// Rows below the headline: state, then whatever of capacity/time we can say.
|
|
static int battery_detail_rows(const DesktopState* ds) {
|
|
const montauk::abi::BatteryInfo& b = ds->battery_info;
|
|
if (!b.present) return 0;
|
|
|
|
int rows = 2; // "Charge", then "State"
|
|
if (b.minutesRemaining >= 0) rows++; // "Time left"
|
|
if (b.fullCapacity > 0) rows++; // "Capacity"
|
|
return rows;
|
|
}
|
|
|
|
static int battery_popup_h(const DesktopState* ds) {
|
|
int fh = system_font_height();
|
|
int header = fh + 10; // title row
|
|
int rows = battery_detail_rows(ds) * (fh + 6);
|
|
return 10 + header + rows + 12;
|
|
}
|
|
|
|
static Rect battery_popup_rect(const DesktopState* ds) {
|
|
return {battery_popup_x(ds), PANEL_HEIGHT + 2, BATT_POPUP_W, battery_popup_h(ds)};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Icon selection
|
|
// ============================================================================
|
|
|
|
// The icon pack ships one glyph per 10% step. Round to the nearest step so the
|
|
// drawn level matches the number in the popout rather than always sitting a
|
|
// bucket low.
|
|
static int battery_bucket(int percent) {
|
|
if (percent < 0) percent = 0;
|
|
if (percent > 100) percent = 100;
|
|
int bucket = ((percent + 5) / 10) * 10;
|
|
return bucket > 100 ? 100 : bucket;
|
|
}
|
|
|
|
static void battery_icon_path(const DesktopState* ds, char* out, int cap) {
|
|
const montauk::abi::BatteryInfo& b = ds->battery_info;
|
|
|
|
if (!b.present) {
|
|
montauk::strncpy(out, "0:/os/icons/battery-missing-symbolic.svg", cap);
|
|
return;
|
|
}
|
|
|
|
int bucket = battery_bucket(b.percent);
|
|
|
|
// A full battery on the charger gets the "charged" glyph; the pack has no
|
|
// 100% charging variant, and "charged" is what that state means anyway.
|
|
if (b.charging && bucket >= 100) {
|
|
montauk::strncpy(out, "0:/os/icons/battery-level-100-charged-symbolic.svg", cap);
|
|
return;
|
|
}
|
|
|
|
snprintf(out, cap, "0:/os/icons/battery-level-%d%s-symbolic.svg",
|
|
bucket, b.charging ? "-charging" : "");
|
|
}
|
|
|
|
// Reload the glyph only when the one we want changes: at a 5 second poll a
|
|
// laptop crosses a bucket boundary every few minutes at most, and each load
|
|
// rasterises an SVG.
|
|
static bool battery_refresh_icon(DesktopState* ds) {
|
|
char path[64];
|
|
battery_icon_path(ds, path, sizeof(path));
|
|
|
|
if (ds->icon_battery.pixels && montauk::streq(path, ds->battery_icon_name))
|
|
return false;
|
|
|
|
SvgIcon loaded = svg_load(path, 16, 16, colors::PANEL_TEXT);
|
|
if (!loaded.pixels) return false;
|
|
|
|
svg_free(ds->icon_battery);
|
|
ds->icon_battery = loaded;
|
|
montauk::strncpy(ds->battery_icon_name, path, sizeof(ds->battery_icon_name));
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Startup and polling
|
|
// ============================================================================
|
|
|
|
void desktop_battery_init(DesktopState* ds) {
|
|
ds->icon_battery = {nullptr, 0, 0};
|
|
ds->battery_icon_name[0] = '\0';
|
|
ds->battery_popup_open = false;
|
|
ds->battery_icon_rect = {0, 0, 0, 0};
|
|
ds->battery_last_poll = 0;
|
|
montauk::memset(&ds->battery_info, 0, sizeof(ds->battery_info));
|
|
|
|
// A machine with no ACPI battery device fails the call outright, and the
|
|
// indicator never appears. This does not change while we are running, so
|
|
// unlike Wi-Fi there is nothing to keep looking for.
|
|
ds->battery_present = montauk::battery_info(&ds->battery_info) == 0;
|
|
if (ds->battery_present) battery_refresh_icon(ds);
|
|
}
|
|
|
|
bool desktop_battery_poll(DesktopState* ds, uint64_t now) {
|
|
if (!ds->battery_present) return false;
|
|
if (ds->battery_last_poll != 0 && now - ds->battery_last_poll < BATT_POLL_MS)
|
|
return false;
|
|
ds->battery_last_poll = now;
|
|
|
|
montauk::abi::BatteryInfo fresh;
|
|
if (montauk::battery_info(&fresh) != 0) return false;
|
|
|
|
bool changed = fresh.percent != ds->battery_info.percent
|
|
|| fresh.present != ds->battery_info.present
|
|
|| fresh.charging != ds->battery_info.charging
|
|
|| fresh.acOnline != ds->battery_info.acOnline
|
|
|| fresh.minutesRemaining != ds->battery_info.minutesRemaining;
|
|
|
|
ds->battery_info = fresh;
|
|
if (battery_refresh_icon(ds)) changed = true;
|
|
|
|
// While the popout is up its numbers are on screen, so any change at all
|
|
// has to be repainted.
|
|
return changed || ds->battery_popup_open;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Popout
|
|
// ============================================================================
|
|
|
|
|
|
void desktop_draw_battery_popup(DesktopState* ds) {
|
|
Framebuffer& fb = ds->fb;
|
|
const montauk::abi::BatteryInfo& b = ds->battery_info;
|
|
Rect popup = battery_popup_rect(ds);
|
|
int fh = system_font_height();
|
|
Color dim = Color::from_rgb(0x66, 0x66, 0x66);
|
|
|
|
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 lx = popup.x + 14;
|
|
int vx = popup.x + 92;
|
|
int ty = popup.y + 10;
|
|
|
|
draw_text(fb, lx, ty, "Battery", colors::TEXT_COLOR);
|
|
|
|
// A coloured dot mirrors the Wi-Fi popup: green on the charger, amber when
|
|
// the charge is getting short, red once the firmware calls it critical.
|
|
const char* status_str;
|
|
Color dot_color;
|
|
if (!b.present) {
|
|
status_str = "Not present";
|
|
dot_color = Color::from_rgb(0x88, 0x88, 0x88);
|
|
} else if (b.charging) {
|
|
status_str = "Charging";
|
|
dot_color = Color::from_rgb(0x4C, 0xAF, 0x50);
|
|
} else if (b.critical || b.percent <= 10) {
|
|
status_str = "Low";
|
|
dot_color = Color::from_rgb(0xCC, 0x33, 0x33);
|
|
} else if (b.acOnline) {
|
|
status_str = "Plugged in";
|
|
dot_color = Color::from_rgb(0x4C, 0xAF, 0x50);
|
|
} else {
|
|
status_str = "On battery";
|
|
dot_color = (b.percent <= 25) ? Color::from_rgb(0xD0, 0x9A, 0x2E)
|
|
: Color::from_rgb(0x66, 0x66, 0x66);
|
|
}
|
|
|
|
int sw = text_width(status_str);
|
|
int status_x = popup.x + popup.w - 14 - sw;
|
|
fill_circle(fb, status_x - 9, ty + fh / 2, 4, dot_color);
|
|
draw_text(fb, status_x, ty, status_str, dim);
|
|
|
|
ty += fh + 10;
|
|
|
|
if (!b.present) return;
|
|
|
|
char value[40];
|
|
|
|
snprintf(value, sizeof(value), "%d%%", (int)b.percent);
|
|
draw_text(fb, lx, ty, "Charge", dim);
|
|
draw_text(fb, vx, ty, value, colors::TEXT_COLOR);
|
|
ty += fh + 6;
|
|
|
|
snprintf(value, sizeof(value), "%s",
|
|
b.charging ? "Charging" : (b.acOnline ? "Not charging" : "Discharging"));
|
|
draw_text(fb, lx, ty, "State", dim);
|
|
draw_text(fb, vx, ty, value, colors::TEXT_COLOR);
|
|
ty += fh + 6;
|
|
|
|
if (b.minutesRemaining >= 0) {
|
|
int mins = b.minutesRemaining;
|
|
if (mins >= 60) snprintf(value, sizeof(value), "%dh %02dm", mins / 60, mins % 60);
|
|
else snprintf(value, sizeof(value), "%dm", mins);
|
|
draw_text(fb, lx, ty, "Time left", dim);
|
|
draw_text(fb, vx, ty, value, colors::TEXT_COLOR);
|
|
ty += fh + 6;
|
|
}
|
|
|
|
if (b.fullCapacity > 0) {
|
|
const char* unit = b.powerUnitMilliAmps ? "mAh" : "mWh";
|
|
snprintf(value, sizeof(value), "%u/%u %s",
|
|
(unsigned)b.remainingCapacity, (unsigned)b.fullCapacity, unit);
|
|
draw_text(fb, lx, ty, "Capacity", dim);
|
|
draw_text(fb, vx, ty, value, colors::TEXT_COLOR);
|
|
}
|
|
}
|
|
|
|
// A click anywhere outside the popout closes it; the popout itself has nothing
|
|
// to click, so it only has to swallow the event.
|
|
bool desktop_battery_handle_mouse(DesktopState* ds, int mx, int my) {
|
|
if (!ds->battery_popup_open) return false;
|
|
|
|
if (battery_popup_rect(ds).contains(mx, my)) return true;
|
|
|
|
if (!ds->battery_icon_rect.contains(mx, my))
|
|
ds->battery_popup_open = false;
|
|
return false;
|
|
}
|