feat: add directional window resizing

feat: support multi-battery monitoring
fix: prevent windows from exceeding layout bounds
refactor: adjust snap constraints for borders and title bars
This commit is contained in:
retoor 2026-01-26 20:40:08 +01:00
parent f4af2b1f1e
commit ea12677dac
11 changed files with 211 additions and 34 deletions

BIN
bin/dwn

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -25,6 +25,11 @@
#define MAX_NOTIFICATIONS 32
#define MAX_KEYBINDINGS 64
#define RESIZE_LEFT 1
#define RESIZE_RIGHT 2
#define RESIZE_TOP 4
#define RESIZE_BOTTOM 8
#define DEFAULT_BORDER_WIDTH 0
#define DEFAULT_TITLE_HEIGHT 28
#define DEFAULT_PANEL_HEIGHT 32
@ -168,6 +173,7 @@ typedef struct {
int drag_orig_x, drag_orig_y;
int drag_orig_w, drag_orig_h;
bool resizing;
int drag_direction;
Client *pending_focus_client;
long pending_focus_time;

View File

@ -11,6 +11,7 @@
#include <stdbool.h>
#define MAX_TRAY_ICONS 32
#define MAX_BATTERIES 4
#define TRAY_ICON_SIZE 22
#define TRAY_ICON_SPACING 4
@ -59,6 +60,22 @@ typedef struct {
int time_remaining;
} BatteryState;
typedef struct {
char name[32];
bool present;
bool charging;
int percentage;
unsigned long energy_now;
unsigned long energy_full;
} SingleBattery;
typedef struct {
int count;
SingleBattery batteries[MAX_BATTERIES];
bool any_charging;
int combined_percentage;
} MultiBatteryState;
typedef struct {
Window window;
int x, y;
@ -70,6 +87,7 @@ typedef struct {
extern WifiState wifi_state;
extern AudioState audio_state;
extern BatteryState battery_state;
extern MultiBatteryState multi_battery_state;
extern VolumeSlider *volume_slider;
void systray_init(void);
@ -106,6 +124,7 @@ void systray_lock(void);
void systray_unlock(void);
BatteryState systray_get_battery_snapshot(void);
MultiBatteryState systray_get_multi_battery_snapshot(void);
AudioState systray_get_audio_snapshot(void);
void xembed_init(void);

View File

@ -84,6 +84,7 @@ void layout_arrange_tiling(int workspace)
}
int stack_width = area_width - master_width - 3 * gap;
if (stack_width < 50) stack_width = 50;
int i = 0;
int master_y = area_y + gap;
@ -120,6 +121,16 @@ void layout_arrange_tiling(int workspace)
stack_y += h + gap;
}
int max_y = area_y + area_height - gap;
int max_x = area_x + area_width - gap;
if (y + h > max_y) {
h = max_y - y;
}
if (x + w > max_x) {
w = max_x - x;
}
int actual_h = h - title_height - 2 * border;
int actual_w = w - 2 * border;
@ -268,21 +279,25 @@ void layout_apply_snap_constraint(Client *c, int area_x, int area_y,
return;
}
int title_h = (dwn->config && dwn->config->show_decorations) ?
config_get_title_height() : 0;
int border = c->border_width;
int half_w = area_w / 2;
int half_h = area_h / 2;
switch (c->snap.horizontal) {
case SNAP_H_LEFT:
c->x = area_x + gap;
c->width = half_w - gap * 2;
c->x = area_x + gap + border;
c->width = half_w - gap * 2 - 2 * border;
break;
case SNAP_H_RIGHT:
c->x = area_x + half_w + gap;
c->width = half_w - gap * 2;
c->x = area_x + half_w + gap + border;
c->width = half_w - gap * 2 - 2 * border;
break;
case SNAP_H_FULL:
c->x = area_x + gap;
c->width = area_w - gap * 2;
c->x = area_x + gap + border;
c->width = area_w - gap * 2 - 2 * border;
break;
case SNAP_H_NONE:
break;
@ -290,16 +305,16 @@ void layout_apply_snap_constraint(Client *c, int area_x, int area_y,
switch (c->snap.vertical) {
case SNAP_V_TOP:
c->y = area_y + gap;
c->height = half_h - gap * 2;
c->y = area_y + gap + title_h + border;
c->height = half_h - gap * 2 - title_h - 2 * border;
break;
case SNAP_V_BOTTOM:
c->y = area_y + half_h + gap;
c->height = half_h - gap * 2;
c->y = area_y + half_h + gap + title_h + border;
c->height = half_h - gap * 2 - title_h - 2 * border;
break;
case SNAP_V_FULL:
c->y = area_y + gap;
c->height = area_h - gap * 2;
c->y = area_y + gap + title_h + border;
c->height = area_h - gap * 2 - title_h - 2 * border;
break;
case SNAP_V_NONE:
break;

View File

@ -467,6 +467,7 @@ static void handle_button_press(XButtonEvent *ev)
dwn->drag_orig_x = c->x;
dwn->drag_orig_y = c->y;
dwn->resizing = false;
dwn->drag_direction = 0;
api_emit_drag_started(c->window, false);
@ -488,6 +489,7 @@ static void handle_button_press(XButtonEvent *ev)
dwn->drag_orig_w = c->width;
dwn->drag_orig_h = c->height;
dwn->resizing = true;
dwn->drag_direction = direction;
api_emit_drag_started(c->window, true);
@ -545,11 +547,36 @@ static void handle_motion_notify(XMotionEvent *ev)
int dy = ev->y_root - dwn->drag_start_y;
if (dwn->resizing) {
int new_w = dwn->drag_orig_w + dx;
int new_h = dwn->drag_orig_h + dy;
if (new_w < 50) new_w = 50;
if (new_h < 50) new_h = 50;
client_resize(c, new_w, new_h);
int dir = dwn->drag_direction;
int new_x = dwn->drag_orig_x;
int new_y = dwn->drag_orig_y;
int new_w = dwn->drag_orig_w;
int new_h = dwn->drag_orig_h;
if (dir & RESIZE_LEFT) {
new_x = dwn->drag_orig_x + dx;
new_w = dwn->drag_orig_w - dx;
} else if (dir & RESIZE_RIGHT) {
new_w = dwn->drag_orig_w + dx;
}
if (dir & RESIZE_TOP) {
new_y = dwn->drag_orig_y + dy;
new_h = dwn->drag_orig_h - dy;
} else if (dir & RESIZE_BOTTOM) {
new_h = dwn->drag_orig_h + dy;
}
if (new_w < 50) {
if (dir & RESIZE_LEFT) new_x = dwn->drag_orig_x + dwn->drag_orig_w - 50;
new_w = 50;
}
if (new_h < 50) {
if (dir & RESIZE_TOP) new_y = dwn->drag_orig_y + dwn->drag_orig_h - 50;
new_h = 50;
}
client_move_resize(c, new_x, new_y, new_w, new_h);
} else {
client_move(c, dwn->drag_orig_x + dx, dwn->drag_orig_y + dy);
}

View File

@ -790,10 +790,23 @@ static int panel_calculate_stats_width(void)
sys_stats.load_1min, sys_stats.load_5min, sys_stats.load_15min);
total += panel_text_width(buf, len) + WIDGET_SPACING;
BatteryState bat_snap = systray_get_battery_snapshot();
if (bat_snap.present) {
const char *bat_icon = bat_snap.charging ? "[+]" : "[=]";
len = snprintf(buf, sizeof(buf), "%s%d%%", bat_icon, bat_snap.percentage);
MultiBatteryState multi_bat = systray_get_multi_battery_snapshot();
if (multi_bat.count > 0) {
const char *bat_icon = multi_bat.any_charging ? "[+]" : "[=]";
if (multi_bat.count == 1) {
len = snprintf(buf, sizeof(buf), "%s%d%%", bat_icon, multi_bat.combined_percentage);
} else {
int offset = snprintf(buf, sizeof(buf), "%s%d%% (", bat_icon, multi_bat.combined_percentage);
for (int i = 0; i < multi_bat.count && offset < (int)sizeof(buf) - 10; i++) {
if (i > 0) {
offset += snprintf(buf + offset, sizeof(buf) - offset, " ");
}
offset += snprintf(buf + offset, sizeof(buf) - offset, "B%d:%d%%",
i + 1, multi_bat.batteries[i].percentage);
}
snprintf(buf + offset, sizeof(buf) - offset, ")");
len = strlen(buf);
}
total += panel_text_width(buf, len) + WIDGET_SPACING;
}
@ -859,19 +872,32 @@ static void panel_render_system_stats(Panel *panel, int x, int *width)
panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, load_color);
current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING;
BatteryState bat_snap = systray_get_battery_snapshot();
if (bat_snap.present) {
MultiBatteryState multi_bat = systray_get_multi_battery_snapshot();
if (multi_bat.count > 0) {
unsigned long bat_color = colors->panel_fg;
if (bat_snap.percentage <= 20 && !bat_snap.charging) {
if (multi_bat.combined_percentage <= 20 && !multi_bat.any_charging) {
bat_color = colors->workspace_urgent;
} else if (bat_snap.percentage <= 40 && !bat_snap.charging) {
} else if (multi_bat.combined_percentage <= 40 && !multi_bat.any_charging) {
bat_color = 0xFFA500;
} else if (bat_snap.charging) {
} else if (multi_bat.any_charging) {
bat_color = colors->workspace_active;
}
const char *bat_icon = bat_snap.charging ? "[+]" : "[=]";
len = snprintf(stats_buf, sizeof(stats_buf), "%s%d%%", bat_icon, bat_snap.percentage);
const char *bat_icon = multi_bat.any_charging ? "[+]" : "[=]";
if (multi_bat.count == 1) {
len = snprintf(stats_buf, sizeof(stats_buf), "%s%d%%", bat_icon, multi_bat.combined_percentage);
} else {
int offset = snprintf(stats_buf, sizeof(stats_buf), "%s%d%% (", bat_icon, multi_bat.combined_percentage);
for (int i = 0; i < multi_bat.count && offset < (int)sizeof(stats_buf) - 10; i++) {
if (i > 0) {
offset += snprintf(stats_buf + offset, sizeof(stats_buf) - offset, " ");
}
offset += snprintf(stats_buf + offset, sizeof(stats_buf) - offset, "B%d:%d%%",
i + 1, multi_bat.batteries[i].percentage);
}
snprintf(stats_buf + offset, sizeof(stats_buf) - offset, ")");
len = strlen(stats_buf);
}
panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, bat_color);
current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING;
}

View File

@ -43,6 +43,7 @@
WifiState wifi_state = {0};
AudioState audio_state = {0};
BatteryState battery_state = {0};
MultiBatteryState multi_battery_state = {0};
VolumeSlider *volume_slider = NULL;
TrayIcon tray_icons[MAX_TRAY_ICONS];
@ -183,6 +184,7 @@ void battery_update_state(void)
char value[64];
FILE *fp;
memset(&multi_battery_state, 0, sizeof(multi_battery_state));
battery_state.present = false;
battery_state.charging = false;
battery_state.percentage = 0;
@ -192,24 +194,75 @@ void battery_update_state(void)
return;
}
unsigned long total_energy_now = 0;
unsigned long total_energy_full = 0;
bool any_charging = false;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.') {
continue;
}
if (multi_battery_state.count >= MAX_BATTERIES) {
break;
}
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/type", entry->d_name);
fp = fopen(path, "r");
if (fp != NULL) {
if (fgets(value, sizeof(value), fp) != NULL) {
value[strcspn(value, "\n")] = '\0';
if (strcmp(value, "Battery") == 0) {
battery_state.present = true;
SingleBattery *bat = &multi_battery_state.batteries[multi_battery_state.count];
bat->present = true;
size_t name_len = strlen(entry->d_name);
if (name_len >= sizeof(bat->name)) {
name_len = sizeof(bat->name) - 1;
}
memcpy(bat->name, entry->d_name, name_len);
bat->name[name_len] = '\0';
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/energy_now", entry->d_name);
FILE *energy_fp = fopen(path, "r");
if (energy_fp != NULL) {
if (fgets(value, sizeof(value), energy_fp) != NULL) {
bat->energy_now = strtoul(value, NULL, 10);
}
fclose(energy_fp);
} else {
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/charge_now", entry->d_name);
energy_fp = fopen(path, "r");
if (energy_fp != NULL) {
if (fgets(value, sizeof(value), energy_fp) != NULL) {
bat->energy_now = strtoul(value, NULL, 10);
}
fclose(energy_fp);
}
}
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/energy_full", entry->d_name);
energy_fp = fopen(path, "r");
if (energy_fp != NULL) {
if (fgets(value, sizeof(value), energy_fp) != NULL) {
bat->energy_full = strtoul(value, NULL, 10);
}
fclose(energy_fp);
} else {
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/charge_full", entry->d_name);
energy_fp = fopen(path, "r");
if (energy_fp != NULL) {
if (fgets(value, sizeof(value), energy_fp) != NULL) {
bat->energy_full = strtoul(value, NULL, 10);
}
fclose(energy_fp);
}
}
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/capacity", entry->d_name);
FILE *cap_fp = fopen(path, "r");
if (cap_fp != NULL) {
if (fgets(value, sizeof(value), cap_fp) != NULL) {
battery_state.percentage = atoi(value);
bat->percentage = atoi(value);
}
fclose(cap_fp);
}
@ -219,20 +272,42 @@ void battery_update_state(void)
if (status_fp != NULL) {
if (fgets(value, sizeof(value), status_fp) != NULL) {
value[strcspn(value, "\n")] = '\0';
battery_state.charging = (strcmp(value, "Charging") == 0 ||
strcmp(value, "Full") == 0);
bat->charging = (strcmp(value, "Charging") == 0 ||
strcmp(value, "Full") == 0);
if (bat->charging) {
any_charging = true;
}
}
fclose(status_fp);
}
fclose(fp);
break;
total_energy_now += bat->energy_now;
total_energy_full += bat->energy_full;
multi_battery_state.count++;
}
}
fclose(fp);
}
}
closedir(dir);
multi_battery_state.any_charging = any_charging;
if (total_energy_full > 0) {
multi_battery_state.combined_percentage = (int)((total_energy_now * 100) / total_energy_full);
} else if (multi_battery_state.count > 0) {
int total_pct = 0;
for (int i = 0; i < multi_battery_state.count; i++) {
total_pct += multi_battery_state.batteries[i].percentage;
}
multi_battery_state.combined_percentage = total_pct / multi_battery_state.count;
}
if (multi_battery_state.count > 0) {
battery_state.present = true;
battery_state.charging = any_charging;
battery_state.percentage = multi_battery_state.combined_percentage;
}
}
const char *battery_get_icon(void)
@ -707,6 +782,15 @@ BatteryState systray_get_battery_snapshot(void)
return snapshot;
}
MultiBatteryState systray_get_multi_battery_snapshot(void)
{
MultiBatteryState snapshot;
pthread_mutex_lock(&state_mutex);
snapshot = multi_battery_state;
pthread_mutex_unlock(&state_mutex);
return snapshot;
}
AudioState systray_get_audio_snapshot(void)
{
AudioState snapshot;