|
/*
|
|
* DWN - Desktop Window Manager
|
|
* retoor <retoor@molodetz.nl>
|
|
* System tray widgets implementation with UTF-8 support
|
|
*/
|
|
|
|
#include "systray.h"
|
|
#include "panel.h"
|
|
#include "config.h"
|
|
#include "util.h"
|
|
#include "notifications.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
#include <pthread.h>
|
|
|
|
#define ICON_WIFI_FULL "\xE2\x96\x82\xE2\x96\x84\xE2\x96\x86\xE2\x96\x88"
|
|
#define ICON_WIFI_OFF "\xE2\x9C\x97"
|
|
#define ICON_VOLUME_HIGH "\xF0\x9F\x94\x8A"
|
|
#define ICON_VOLUME_MED "\xF0\x9F\x94\x89"
|
|
#define ICON_VOLUME_LOW "\xF0\x9F\x94\x88"
|
|
#define ICON_VOLUME_MUTE "\xF0\x9F\x94\x87"
|
|
#define ICON_BATTERY_FULL "\xF0\x9F\x94\x8B"
|
|
#define ICON_BATTERY_CHARGE "\xE2\x9A\xA1"
|
|
|
|
#define ASCII_WIFI_ON "W"
|
|
#define ASCII_WIFI_OFF "-"
|
|
#define ASCII_VOL_HIGH "V"
|
|
#define ASCII_VOL_MUTE "M"
|
|
#define ASCII_BATTERY "B"
|
|
#define ASCII_CHARGING "+"
|
|
|
|
#define SYSTRAY_SPACING 12
|
|
#define DROPDOWN_ITEM_HEIGHT 28
|
|
#define DROPDOWN_WIDTH 400
|
|
#define DROPDOWN_PADDING 8
|
|
#define SLIDER_WIDTH 30
|
|
#define SLIDER_HEIGHT 120
|
|
#define SLIDER_PADDING 8
|
|
#define SLIDER_KNOB_HEIGHT 8
|
|
|
|
#define WIFI_SCAN_INTERVAL 10000
|
|
|
|
WifiState wifi_state = {0};
|
|
AudioState audio_state = {0};
|
|
BatteryState battery_state = {0};
|
|
DropdownMenu *wifi_menu = NULL;
|
|
VolumeSlider *volume_slider = NULL;
|
|
|
|
static pthread_t update_thread;
|
|
static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
static volatile int thread_running = 0;
|
|
|
|
static int systray_x = 0;
|
|
static int systray_width = 0;
|
|
static int wifi_icon_x = 0;
|
|
static int audio_icon_x = 0;
|
|
|
|
static void wifi_update_state_internal(void);
|
|
static void audio_update_state_internal(void);
|
|
|
|
|
|
static void draw_utf8_text(Drawable d, int x, int y, const char *text, unsigned long color)
|
|
{
|
|
if (dwn == NULL || dwn->display == NULL || text == NULL) {
|
|
return;
|
|
}
|
|
|
|
if (dwn->xft_font != NULL) {
|
|
XftDraw *xft_draw = XftDrawCreate(dwn->display, d,
|
|
DefaultVisual(dwn->display, dwn->screen),
|
|
dwn->colormap);
|
|
if (xft_draw != NULL) {
|
|
XftColor xft_color;
|
|
XRenderColor render_color;
|
|
|
|
render_color.red = ((color >> 16) & 0xFF) * 257;
|
|
render_color.green = ((color >> 8) & 0xFF) * 257;
|
|
render_color.blue = (color & 0xFF) * 257;
|
|
render_color.alpha = 0xFFFF;
|
|
|
|
XftColorAllocValue(dwn->display,
|
|
DefaultVisual(dwn->display, dwn->screen),
|
|
dwn->colormap, &render_color, &xft_color);
|
|
|
|
XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font,
|
|
x, y, (const FcChar8 *)text, strlen(text));
|
|
|
|
XftColorFree(dwn->display,
|
|
DefaultVisual(dwn->display, dwn->screen),
|
|
dwn->colormap, &xft_color);
|
|
XftDrawDestroy(xft_draw);
|
|
return;
|
|
}
|
|
}
|
|
|
|
XSetForeground(dwn->display, dwn->gc, color);
|
|
XDrawString(dwn->display, d, dwn->gc, x, y, text, strlen(text));
|
|
}
|
|
|
|
static int get_text_width(const char *text)
|
|
{
|
|
if (dwn == NULL || text == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
if (dwn->xft_font != NULL) {
|
|
XGlyphInfo extents;
|
|
XftTextExtentsUtf8(dwn->display, dwn->xft_font,
|
|
(const FcChar8 *)text, strlen(text), &extents);
|
|
return extents.xOff;
|
|
}
|
|
|
|
if (dwn->font != NULL) {
|
|
return XTextWidth(dwn->font, text, strlen(text));
|
|
}
|
|
|
|
return strlen(text) * 8;
|
|
}
|
|
|
|
|
|
static void *systray_update_thread(void *arg)
|
|
{
|
|
(void)arg;
|
|
|
|
while (thread_running) {
|
|
wifi_update_state_internal();
|
|
|
|
audio_update_state_internal();
|
|
|
|
pthread_mutex_lock(&state_mutex);
|
|
battery_update_state();
|
|
pthread_mutex_unlock(&state_mutex);
|
|
|
|
for (int i = 0; i < 20 && thread_running; i++) {
|
|
usleep(100000);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void systray_init(void)
|
|
{
|
|
memset(&wifi_state, 0, sizeof(wifi_state));
|
|
memset(&audio_state, 0, sizeof(audio_state));
|
|
memset(&battery_state, 0, sizeof(battery_state));
|
|
|
|
audio_state.volume = 50;
|
|
wifi_state.enabled = true;
|
|
|
|
thread_running = 1;
|
|
if (pthread_create(&update_thread, NULL, systray_update_thread, NULL) != 0) {
|
|
LOG_WARN("Failed to create systray update thread");
|
|
thread_running = 0;
|
|
}
|
|
|
|
LOG_INFO("System tray initialized");
|
|
}
|
|
|
|
void systray_cleanup(void)
|
|
{
|
|
if (thread_running) {
|
|
thread_running = 0;
|
|
pthread_join(update_thread, NULL);
|
|
}
|
|
|
|
if (wifi_menu != NULL) {
|
|
dropdown_destroy(wifi_menu);
|
|
wifi_menu = NULL;
|
|
}
|
|
if (volume_slider != NULL) {
|
|
volume_slider_destroy(volume_slider);
|
|
volume_slider = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
void battery_update_state(void)
|
|
{
|
|
DIR *dir;
|
|
struct dirent *entry;
|
|
char path[512];
|
|
char value[64];
|
|
FILE *fp;
|
|
|
|
battery_state.present = false;
|
|
battery_state.charging = false;
|
|
battery_state.percentage = 0;
|
|
|
|
dir = opendir("/sys/class/power_supply");
|
|
if (dir == NULL) {
|
|
return;
|
|
}
|
|
|
|
while ((entry = readdir(dir)) != NULL) {
|
|
if (entry->d_name[0] == '.') {
|
|
continue;
|
|
}
|
|
|
|
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;
|
|
|
|
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);
|
|
}
|
|
fclose(cap_fp);
|
|
}
|
|
|
|
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/status", entry->d_name);
|
|
FILE *status_fp = fopen(path, "r");
|
|
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);
|
|
}
|
|
fclose(status_fp);
|
|
}
|
|
|
|
fclose(fp);
|
|
break;
|
|
}
|
|
}
|
|
fclose(fp);
|
|
}
|
|
}
|
|
closedir(dir);
|
|
}
|
|
|
|
const char *battery_get_icon(void)
|
|
{
|
|
if (!battery_state.present) {
|
|
return "";
|
|
}
|
|
|
|
if (battery_state.charging) {
|
|
return ASCII_CHARGING;
|
|
}
|
|
|
|
return ASCII_BATTERY;
|
|
}
|
|
|
|
|
|
static void wifi_update_state_internal(void)
|
|
{
|
|
FILE *fp;
|
|
char line[256];
|
|
WifiState temp_state = {0};
|
|
|
|
fp = popen("nmcli -t -f GENERAL.STATE,GENERAL.CONNECTION dev show 2>/dev/null | head -2", "r");
|
|
if (fp != NULL) {
|
|
while (fgets(line, sizeof(line), fp) != NULL) {
|
|
line[strcspn(line, "\n")] = '\0';
|
|
|
|
if (strstr(line, "GENERAL.STATE:") != NULL) {
|
|
if (strstr(line, "100 (connected)") != NULL) {
|
|
temp_state.connected = true;
|
|
temp_state.enabled = true;
|
|
}
|
|
}
|
|
if (strstr(line, "GENERAL.CONNECTION:") != NULL) {
|
|
char *ssid = strchr(line, ':');
|
|
if (ssid != NULL && strlen(ssid) > 1) {
|
|
ssid++;
|
|
strncpy(temp_state.current_ssid, ssid, sizeof(temp_state.current_ssid) - 1);
|
|
}
|
|
}
|
|
}
|
|
pclose(fp);
|
|
}
|
|
|
|
if (temp_state.connected) {
|
|
fp = popen("nmcli -t -f IN-USE,SIGNAL dev wifi list 2>/dev/null | grep '^\\*' | cut -d: -f2", "r");
|
|
if (fp != NULL) {
|
|
if (fgets(line, sizeof(line), fp) != NULL) {
|
|
temp_state.signal_strength = atoi(line);
|
|
}
|
|
pclose(fp);
|
|
}
|
|
}
|
|
|
|
pthread_mutex_lock(&state_mutex);
|
|
wifi_state.connected = temp_state.connected;
|
|
wifi_state.enabled = temp_state.enabled;
|
|
wifi_state.signal_strength = temp_state.signal_strength;
|
|
memcpy(wifi_state.current_ssid, temp_state.current_ssid, sizeof(wifi_state.current_ssid));
|
|
pthread_mutex_unlock(&state_mutex);
|
|
}
|
|
|
|
void wifi_update_state(void)
|
|
{
|
|
}
|
|
|
|
void wifi_scan_networks(void)
|
|
{
|
|
FILE *fp;
|
|
char line[512];
|
|
|
|
WifiNetwork temp_networks[MAX_WIFI_NETWORKS];
|
|
int temp_count = 0;
|
|
long scan_time = get_time_ms();
|
|
|
|
fp = popen("nmcli -t -f SSID,SIGNAL,SECURITY,IN-USE dev wifi list 2>/dev/null", "r");
|
|
if (fp == NULL) {
|
|
LOG_WARN("Failed to scan WiFi networks");
|
|
return;
|
|
}
|
|
|
|
while (fgets(line, sizeof(line), fp) != NULL && temp_count < MAX_WIFI_NETWORKS) {
|
|
line[strcspn(line, "\n")] = '\0';
|
|
|
|
char *ssid = strtok(line, ":");
|
|
char *signal = strtok(NULL, ":");
|
|
char *security = strtok(NULL, ":");
|
|
char *in_use = strtok(NULL, ":");
|
|
|
|
if (ssid != NULL && strlen(ssid) > 0) {
|
|
WifiNetwork *net = &temp_networks[temp_count];
|
|
strncpy(net->ssid, ssid, sizeof(net->ssid) - 1);
|
|
net->ssid[sizeof(net->ssid) - 1] = '\0';
|
|
net->signal = signal ? atoi(signal) : 0;
|
|
strncpy(net->security, security ? security : "", sizeof(net->security) - 1);
|
|
net->security[sizeof(net->security) - 1] = '\0';
|
|
net->connected = (in_use != NULL && in_use[0] == '*');
|
|
temp_count++;
|
|
}
|
|
}
|
|
pclose(fp);
|
|
|
|
pthread_mutex_lock(&state_mutex);
|
|
memcpy(wifi_state.networks, temp_networks, sizeof(temp_networks));
|
|
wifi_state.network_count = temp_count;
|
|
wifi_state.last_scan = scan_time;
|
|
pthread_mutex_unlock(&state_mutex);
|
|
|
|
LOG_DEBUG("Scanned %d WiFi networks", temp_count);
|
|
}
|
|
|
|
void wifi_connect(const char *ssid)
|
|
{
|
|
if (ssid == NULL || strlen(ssid) == 0) {
|
|
return;
|
|
}
|
|
|
|
char cmd[256];
|
|
snprintf(cmd, sizeof(cmd), "nmcli dev wifi connect \"%s\" &", ssid);
|
|
|
|
char msg[128];
|
|
snprintf(msg, sizeof(msg), "Connecting to %s...", ssid);
|
|
notification_show("WiFi", "Connecting", msg, NULL, 3000);
|
|
|
|
spawn_async(cmd);
|
|
|
|
pthread_mutex_lock(&state_mutex);
|
|
wifi_state.connected = false;
|
|
pthread_mutex_unlock(&state_mutex);
|
|
}
|
|
|
|
void wifi_disconnect(void)
|
|
{
|
|
spawn_async("nmcli dev disconnect wlan0 &");
|
|
|
|
pthread_mutex_lock(&state_mutex);
|
|
wifi_state.connected = false;
|
|
pthread_mutex_unlock(&state_mutex);
|
|
|
|
notification_show("WiFi", "Disconnected", "WiFi connection closed", NULL, 2000);
|
|
}
|
|
|
|
const char *wifi_get_icon(void)
|
|
{
|
|
if (!wifi_state.enabled || !wifi_state.connected) {
|
|
return ASCII_WIFI_OFF;
|
|
}
|
|
return ASCII_WIFI_ON;
|
|
}
|
|
|
|
|
|
static void audio_update_state_internal(void)
|
|
{
|
|
FILE *fp;
|
|
char line[256];
|
|
int volume = 50;
|
|
bool muted = false;
|
|
|
|
fp = popen("amixer get Master 2>/dev/null | grep -o '[0-9]*%\\|\\[on\\]\\|\\[off\\]' | head -2", "r");
|
|
if (fp != NULL) {
|
|
while (fgets(line, sizeof(line), fp) != NULL) {
|
|
line[strcspn(line, "\n")] = '\0';
|
|
|
|
if (strchr(line, '%') != NULL) {
|
|
volume = atoi(line);
|
|
} else if (strcmp(line, "[off]") == 0) {
|
|
muted = true;
|
|
} else if (strcmp(line, "[on]") == 0) {
|
|
muted = false;
|
|
}
|
|
}
|
|
pclose(fp);
|
|
}
|
|
|
|
pthread_mutex_lock(&state_mutex);
|
|
audio_state.volume = volume;
|
|
audio_state.muted = muted;
|
|
pthread_mutex_unlock(&state_mutex);
|
|
}
|
|
|
|
void audio_update_state(void)
|
|
{
|
|
}
|
|
|
|
void audio_set_volume(int volume)
|
|
{
|
|
if (volume < 0) volume = 0;
|
|
if (volume > 100) volume = 100;
|
|
|
|
char cmd[64];
|
|
snprintf(cmd, sizeof(cmd), "amixer set Master %d%% >/dev/null 2>&1 &", volume);
|
|
spawn_async(cmd);
|
|
|
|
pthread_mutex_lock(&state_mutex);
|
|
audio_state.volume = volume;
|
|
audio_state.muted = false;
|
|
pthread_mutex_unlock(&state_mutex);
|
|
}
|
|
|
|
void audio_toggle_mute(void)
|
|
{
|
|
spawn_async("amixer set Master toggle >/dev/null 2>&1 &");
|
|
|
|
pthread_mutex_lock(&state_mutex);
|
|
audio_state.muted = !audio_state.muted;
|
|
pthread_mutex_unlock(&state_mutex);
|
|
}
|
|
|
|
const char *audio_get_icon(void)
|
|
{
|
|
if (audio_state.muted || audio_state.volume == 0) {
|
|
return ASCII_VOL_MUTE;
|
|
}
|
|
return ASCII_VOL_HIGH;
|
|
}
|
|
|
|
|
|
VolumeSlider *volume_slider_create(int x, int y)
|
|
{
|
|
if (dwn == NULL || dwn->display == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
VolumeSlider *slider = dwn_calloc(1, sizeof(VolumeSlider));
|
|
slider->x = x;
|
|
slider->y = y;
|
|
slider->width = SLIDER_WIDTH;
|
|
slider->height = SLIDER_HEIGHT;
|
|
slider->visible = false;
|
|
slider->dragging = false;
|
|
|
|
return slider;
|
|
}
|
|
|
|
void volume_slider_destroy(VolumeSlider *slider)
|
|
{
|
|
if (slider == NULL) {
|
|
return;
|
|
}
|
|
|
|
if (slider->window != None && dwn != NULL && dwn->display != NULL) {
|
|
XDestroyWindow(dwn->display, slider->window);
|
|
}
|
|
|
|
dwn_free(slider);
|
|
}
|
|
|
|
void volume_slider_show(VolumeSlider *slider)
|
|
{
|
|
if (slider == NULL || dwn == NULL || dwn->display == NULL) {
|
|
return;
|
|
}
|
|
|
|
if (slider->window == None) {
|
|
XSetWindowAttributes swa;
|
|
swa.override_redirect = True;
|
|
swa.background_pixel = dwn->config->colors.panel_bg;
|
|
swa.border_pixel = dwn->config->colors.border_focused;
|
|
swa.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask |
|
|
PointerMotionMask | LeaveWindowMask;
|
|
|
|
slider->window = XCreateWindow(dwn->display, dwn->root,
|
|
slider->x, slider->y,
|
|
slider->width, slider->height,
|
|
1,
|
|
CopyFromParent, InputOutput, CopyFromParent,
|
|
CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask,
|
|
&swa);
|
|
}
|
|
|
|
slider->visible = true;
|
|
XMapRaised(dwn->display, slider->window);
|
|
volume_slider_render(slider);
|
|
}
|
|
|
|
void volume_slider_hide(VolumeSlider *slider)
|
|
{
|
|
if (slider == NULL || slider->window == None) {
|
|
return;
|
|
}
|
|
|
|
slider->visible = false;
|
|
slider->dragging = false;
|
|
XUnmapWindow(dwn->display, slider->window);
|
|
}
|
|
|
|
void volume_slider_render(VolumeSlider *slider)
|
|
{
|
|
if (slider == NULL || slider->window == None || !slider->visible) {
|
|
return;
|
|
}
|
|
|
|
Display *dpy = dwn->display;
|
|
const ColorScheme *colors = config_get_colors();
|
|
|
|
XSetForeground(dpy, dwn->gc, colors->panel_bg);
|
|
XFillRectangle(dpy, slider->window, dwn->gc, 0, 0, slider->width, slider->height);
|
|
|
|
int track_x = slider->width / 2 - 2;
|
|
int track_y = SLIDER_PADDING;
|
|
int track_height = slider->height - 2 * SLIDER_PADDING;
|
|
|
|
XSetForeground(dpy, dwn->gc, colors->workspace_inactive);
|
|
XFillRectangle(dpy, slider->window, dwn->gc, track_x, track_y, 4, track_height);
|
|
|
|
int fill_height = (audio_state.volume * track_height) / 100;
|
|
int fill_y = track_y + track_height - fill_height;
|
|
|
|
XSetForeground(dpy, dwn->gc, colors->workspace_active);
|
|
XFillRectangle(dpy, slider->window, dwn->gc, track_x, fill_y, 4, fill_height);
|
|
|
|
int knob_y = fill_y - SLIDER_KNOB_HEIGHT / 2;
|
|
if (knob_y < track_y) knob_y = track_y;
|
|
if (knob_y > track_y + track_height - SLIDER_KNOB_HEIGHT) {
|
|
knob_y = track_y + track_height - SLIDER_KNOB_HEIGHT;
|
|
}
|
|
|
|
XSetForeground(dpy, dwn->gc, colors->panel_fg);
|
|
XFillRectangle(dpy, slider->window, dwn->gc,
|
|
track_x - 4, knob_y, 12, SLIDER_KNOB_HEIGHT);
|
|
|
|
char vol_text[8];
|
|
snprintf(vol_text, sizeof(vol_text), "%d%%", audio_state.volume);
|
|
int text_width = get_text_width(vol_text);
|
|
int text_x = (slider->width - text_width) / 2;
|
|
|
|
draw_utf8_text(slider->window, text_x, slider->height - 4, vol_text, colors->panel_fg);
|
|
|
|
XFlush(dpy);
|
|
}
|
|
|
|
void volume_slider_handle_click(VolumeSlider *slider, int x, int y)
|
|
{
|
|
(void)x;
|
|
|
|
if (slider == NULL || !slider->visible) {
|
|
return;
|
|
}
|
|
|
|
slider->dragging = true;
|
|
|
|
int track_y = SLIDER_PADDING;
|
|
int track_height = slider->height - 2 * SLIDER_PADDING;
|
|
|
|
int relative_y = y - track_y;
|
|
if (relative_y < 0) relative_y = 0;
|
|
if (relative_y > track_height) relative_y = track_height;
|
|
|
|
int new_volume = 100 - (relative_y * 100 / track_height);
|
|
audio_set_volume(new_volume);
|
|
volume_slider_render(slider);
|
|
panel_render_all();
|
|
}
|
|
|
|
void volume_slider_handle_motion(VolumeSlider *slider, int x, int y)
|
|
{
|
|
if (slider == NULL || !slider->visible || !slider->dragging) {
|
|
return;
|
|
}
|
|
|
|
volume_slider_handle_click(slider, x, y);
|
|
}
|
|
|
|
void volume_slider_handle_release(VolumeSlider *slider)
|
|
{
|
|
if (slider == NULL) {
|
|
return;
|
|
}
|
|
slider->dragging = false;
|
|
}
|
|
|
|
|
|
DropdownMenu *dropdown_create(int x, int y, int width)
|
|
{
|
|
if (dwn == NULL || dwn->display == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
DropdownMenu *menu = dwn_calloc(1, sizeof(DropdownMenu));
|
|
menu->x = x;
|
|
menu->y = y;
|
|
menu->width = width;
|
|
menu->height = 0;
|
|
menu->item_count = 0;
|
|
menu->hovered_item = -1;
|
|
menu->visible = false;
|
|
menu->on_select = NULL;
|
|
|
|
return menu;
|
|
}
|
|
|
|
void dropdown_destroy(DropdownMenu *menu)
|
|
{
|
|
if (menu == NULL) {
|
|
return;
|
|
}
|
|
|
|
if (menu->window != None && dwn != NULL && dwn->display != NULL) {
|
|
XDestroyWindow(dwn->display, menu->window);
|
|
}
|
|
|
|
dwn_free(menu);
|
|
}
|
|
|
|
void dropdown_show(DropdownMenu *menu)
|
|
{
|
|
if (menu == NULL || dwn == NULL || dwn->display == NULL) {
|
|
return;
|
|
}
|
|
|
|
wifi_scan_networks();
|
|
|
|
menu->item_count = wifi_state.network_count;
|
|
menu->height = menu->item_count * DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING;
|
|
|
|
if (menu->height < DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING) {
|
|
menu->height = DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING;
|
|
menu->item_count = 1;
|
|
}
|
|
|
|
int max_text_width = 0;
|
|
for (int i = 0; i < wifi_state.network_count; i++) {
|
|
WifiNetwork *net = &wifi_state.networks[i];
|
|
char label[128];
|
|
snprintf(label, sizeof(label), "%s %d%%", net->ssid, net->signal);
|
|
int text_width = get_text_width(label);
|
|
if (text_width > max_text_width) {
|
|
max_text_width = text_width;
|
|
}
|
|
}
|
|
int min_width = 250;
|
|
int calculated_width = max_text_width + 2 * DROPDOWN_PADDING + 30;
|
|
menu->width = (calculated_width > min_width) ? calculated_width : min_width;
|
|
|
|
if (menu->window == None) {
|
|
XSetWindowAttributes swa;
|
|
swa.override_redirect = True;
|
|
swa.background_pixel = dwn->config->colors.panel_bg;
|
|
swa.border_pixel = dwn->config->colors.border_focused;
|
|
swa.event_mask = ExposureMask | ButtonPressMask | PointerMotionMask |
|
|
LeaveWindowMask | EnterWindowMask;
|
|
|
|
menu->window = XCreateWindow(dwn->display, dwn->root,
|
|
menu->x, menu->y,
|
|
menu->width, menu->height,
|
|
1,
|
|
CopyFromParent, InputOutput, CopyFromParent,
|
|
CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask,
|
|
&swa);
|
|
} else {
|
|
XMoveResizeWindow(dwn->display, menu->window,
|
|
menu->x, menu->y, menu->width, menu->height);
|
|
}
|
|
|
|
menu->visible = true;
|
|
XMapRaised(dwn->display, menu->window);
|
|
dropdown_render(menu);
|
|
}
|
|
|
|
void dropdown_hide(DropdownMenu *menu)
|
|
{
|
|
if (menu == NULL || menu->window == None) {
|
|
return;
|
|
}
|
|
|
|
menu->visible = false;
|
|
XUnmapWindow(dwn->display, menu->window);
|
|
}
|
|
|
|
void dropdown_render(DropdownMenu *menu)
|
|
{
|
|
if (menu == NULL || menu->window == None || !menu->visible) {
|
|
return;
|
|
}
|
|
|
|
if (dwn == NULL || dwn->display == NULL) {
|
|
return;
|
|
}
|
|
|
|
Display *dpy = dwn->display;
|
|
const ColorScheme *colors = config_get_colors();
|
|
|
|
int font_height = 12;
|
|
if (dwn->xft_font != NULL) {
|
|
font_height = dwn->xft_font->ascent;
|
|
} else if (dwn->font != NULL) {
|
|
font_height = dwn->font->ascent;
|
|
}
|
|
int text_y_offset = (DROPDOWN_ITEM_HEIGHT + font_height) / 2;
|
|
|
|
XSetForeground(dpy, dwn->gc, colors->panel_bg);
|
|
XFillRectangle(dpy, menu->window, dwn->gc, 0, 0, menu->width, menu->height);
|
|
|
|
if (wifi_state.network_count == 0) {
|
|
draw_utf8_text(menu->window, DROPDOWN_PADDING, DROPDOWN_PADDING + text_y_offset,
|
|
"No networks found", colors->workspace_inactive);
|
|
} else {
|
|
for (int i = 0; i < wifi_state.network_count && i < MAX_WIFI_NETWORKS; i++) {
|
|
WifiNetwork *net = &wifi_state.networks[i];
|
|
int y = DROPDOWN_PADDING + i * DROPDOWN_ITEM_HEIGHT;
|
|
|
|
if (i == menu->hovered_item) {
|
|
XSetForeground(dpy, dwn->gc, colors->workspace_active);
|
|
XFillRectangle(dpy, menu->window, dwn->gc,
|
|
2, y, menu->width - 4, DROPDOWN_ITEM_HEIGHT);
|
|
}
|
|
|
|
unsigned long text_color = (i == menu->hovered_item) ? colors->panel_bg : colors->panel_fg;
|
|
|
|
char label[80];
|
|
if (net->connected) {
|
|
snprintf(label, sizeof(label), "> %s", net->ssid);
|
|
} else {
|
|
snprintf(label, sizeof(label), " %s", net->ssid);
|
|
}
|
|
|
|
if (strlen(label) > 25) {
|
|
label[22] = '.';
|
|
label[23] = '.';
|
|
label[24] = '.';
|
|
label[25] = '\0';
|
|
}
|
|
|
|
draw_utf8_text(menu->window, DROPDOWN_PADDING, y + text_y_offset, label, text_color);
|
|
|
|
char signal[8];
|
|
snprintf(signal, sizeof(signal), "%d%%", net->signal);
|
|
int signal_width = get_text_width(signal);
|
|
int signal_x = menu->width - DROPDOWN_PADDING - signal_width;
|
|
|
|
unsigned long signal_color = (i == menu->hovered_item) ? colors->panel_bg : colors->workspace_inactive;
|
|
draw_utf8_text(menu->window, signal_x, y + text_y_offset, signal, signal_color);
|
|
}
|
|
}
|
|
|
|
XFlush(dpy);
|
|
}
|
|
|
|
int dropdown_hit_test(DropdownMenu *menu, int x, int y)
|
|
{
|
|
(void)x;
|
|
|
|
if (menu == NULL || !menu->visible) {
|
|
return -1;
|
|
}
|
|
|
|
if (y < DROPDOWN_PADDING || y >= menu->height - DROPDOWN_PADDING) {
|
|
return -1;
|
|
}
|
|
|
|
int item = (y - DROPDOWN_PADDING) / DROPDOWN_ITEM_HEIGHT;
|
|
if (item >= 0 && item < menu->item_count) {
|
|
return item;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
void dropdown_handle_click(DropdownMenu *menu, int x, int y)
|
|
{
|
|
if (menu == NULL || !menu->visible) {
|
|
return;
|
|
}
|
|
|
|
int item = dropdown_hit_test(menu, x, y);
|
|
if (item >= 0 && item < wifi_state.network_count) {
|
|
WifiNetwork *net = &wifi_state.networks[item];
|
|
|
|
if (net->connected) {
|
|
wifi_disconnect();
|
|
} else {
|
|
wifi_connect(net->ssid);
|
|
}
|
|
|
|
dropdown_hide(menu);
|
|
}
|
|
}
|
|
|
|
void dropdown_handle_motion(DropdownMenu *menu, int x, int y)
|
|
{
|
|
if (menu == NULL || !menu->visible) {
|
|
return;
|
|
}
|
|
|
|
int old_hovered = menu->hovered_item;
|
|
menu->hovered_item = dropdown_hit_test(menu, x, y);
|
|
|
|
if (menu->hovered_item != old_hovered) {
|
|
dropdown_render(menu);
|
|
}
|
|
}
|
|
|
|
|
|
int systray_get_width(void)
|
|
{
|
|
pthread_mutex_lock(&state_mutex);
|
|
AudioState audio_snap = audio_state;
|
|
WifiState wifi_snap;
|
|
wifi_snap.connected = wifi_state.connected;
|
|
wifi_snap.enabled = wifi_state.enabled;
|
|
wifi_snap.signal_strength = wifi_state.signal_strength;
|
|
memcpy(wifi_snap.current_ssid, wifi_state.current_ssid, sizeof(wifi_snap.current_ssid));
|
|
pthread_mutex_unlock(&state_mutex);
|
|
|
|
char audio_label[32];
|
|
const char *audio_icon = (audio_snap.muted || audio_snap.volume == 0) ? ASCII_VOL_MUTE : ASCII_VOL_HIGH;
|
|
snprintf(audio_label, sizeof(audio_label), "%s%d%%", audio_icon, audio_snap.volume);
|
|
|
|
char wifi_label[128];
|
|
const char *wifi_icon_str = (!wifi_snap.enabled || !wifi_snap.connected) ? ASCII_WIFI_OFF : ASCII_WIFI_ON;
|
|
if (wifi_snap.connected && wifi_snap.current_ssid[0] != '\0') {
|
|
snprintf(wifi_label, sizeof(wifi_label), "%s %s %d%%",
|
|
wifi_icon_str, wifi_snap.current_ssid, wifi_snap.signal_strength);
|
|
} else {
|
|
snprintf(wifi_label, sizeof(wifi_label), "%s", wifi_icon_str);
|
|
}
|
|
|
|
return get_text_width(audio_label) + SYSTRAY_SPACING + get_text_width(wifi_label);
|
|
}
|
|
|
|
void systray_render(Panel *panel, int x, int *width)
|
|
{
|
|
if (panel == NULL || width == NULL) {
|
|
return;
|
|
}
|
|
|
|
const ColorScheme *colors = config_get_colors();
|
|
|
|
pthread_mutex_lock(&state_mutex);
|
|
AudioState audio_snap = audio_state;
|
|
WifiState wifi_snap;
|
|
wifi_snap.connected = wifi_state.connected;
|
|
wifi_snap.enabled = wifi_state.enabled;
|
|
wifi_snap.signal_strength = wifi_state.signal_strength;
|
|
memcpy(wifi_snap.current_ssid, wifi_state.current_ssid, sizeof(wifi_snap.current_ssid));
|
|
pthread_mutex_unlock(&state_mutex);
|
|
|
|
int font_height = 12;
|
|
if (dwn->xft_font != NULL) {
|
|
font_height = dwn->xft_font->ascent;
|
|
} else if (dwn->font != NULL) {
|
|
font_height = dwn->font->ascent;
|
|
}
|
|
int text_y = (panel->height + font_height) / 2;
|
|
|
|
systray_x = x;
|
|
int current_x = x;
|
|
|
|
audio_icon_x = current_x;
|
|
char audio_label[32];
|
|
const char *audio_icon = (audio_snap.muted || audio_snap.volume == 0) ? ASCII_VOL_MUTE : ASCII_VOL_HIGH;
|
|
snprintf(audio_label, sizeof(audio_label), "%s%d%%", audio_icon, audio_snap.volume);
|
|
|
|
unsigned long audio_color = audio_snap.muted ? colors->workspace_inactive : colors->panel_fg;
|
|
draw_utf8_text(panel->buffer, current_x, text_y, audio_label, audio_color);
|
|
current_x += get_text_width(audio_label) + SYSTRAY_SPACING;
|
|
|
|
wifi_icon_x = current_x;
|
|
char wifi_label[128];
|
|
const char *wifi_icon_str = (!wifi_snap.enabled || !wifi_snap.connected) ? ASCII_WIFI_OFF : ASCII_WIFI_ON;
|
|
|
|
if (wifi_snap.connected && wifi_snap.current_ssid[0] != '\0') {
|
|
snprintf(wifi_label, sizeof(wifi_label), "%s %s %d%%",
|
|
wifi_icon_str, wifi_snap.current_ssid, wifi_snap.signal_strength);
|
|
} else {
|
|
snprintf(wifi_label, sizeof(wifi_label), "%s", wifi_icon_str);
|
|
}
|
|
|
|
unsigned long wifi_color = wifi_snap.connected ? colors->panel_fg : colors->workspace_inactive;
|
|
draw_utf8_text(panel->buffer, current_x, text_y, wifi_label, wifi_color);
|
|
current_x += get_text_width(wifi_label);
|
|
|
|
systray_width = current_x - x;
|
|
*width = systray_width;
|
|
}
|
|
|
|
int systray_hit_test(int x)
|
|
{
|
|
if (x >= wifi_icon_x) {
|
|
return 0;
|
|
}
|
|
if (x >= audio_icon_x && x < wifi_icon_x) {
|
|
return 1;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void systray_handle_click(int x, int y, int button)
|
|
{
|
|
int widget = systray_hit_test(x);
|
|
|
|
if (widget == 0) {
|
|
if (volume_slider != NULL && volume_slider->visible) {
|
|
volume_slider_hide(volume_slider);
|
|
}
|
|
|
|
if (button == 1) {
|
|
if (wifi_menu == NULL) {
|
|
int panel_height = config_get_panel_height();
|
|
wifi_menu = dropdown_create(wifi_icon_x, panel_height, DROPDOWN_WIDTH);
|
|
}
|
|
|
|
if (wifi_menu->visible) {
|
|
dropdown_hide(wifi_menu);
|
|
} else {
|
|
wifi_menu->x = wifi_icon_x;
|
|
wifi_menu->y = config_get_panel_height();
|
|
dropdown_show(wifi_menu);
|
|
}
|
|
} else if (button == 3) {
|
|
if (wifi_state.connected) {
|
|
wifi_disconnect();
|
|
}
|
|
}
|
|
} else if (widget == 1) {
|
|
if (wifi_menu != NULL && wifi_menu->visible) {
|
|
dropdown_hide(wifi_menu);
|
|
}
|
|
|
|
if (button == 1) {
|
|
if (volume_slider == NULL) {
|
|
int panel_height = config_get_panel_height();
|
|
volume_slider = volume_slider_create(audio_icon_x, panel_height);
|
|
}
|
|
|
|
if (volume_slider->visible) {
|
|
volume_slider_hide(volume_slider);
|
|
} else {
|
|
volume_slider->x = audio_icon_x;
|
|
volume_slider->y = config_get_panel_height();
|
|
volume_slider_show(volume_slider);
|
|
}
|
|
} else if (button == 3) {
|
|
audio_toggle_mute();
|
|
panel_render_all();
|
|
} else if (button == 4) {
|
|
audio_set_volume(audio_state.volume + 5);
|
|
if (volume_slider != NULL && volume_slider->visible) {
|
|
volume_slider_render(volume_slider);
|
|
}
|
|
panel_render_all();
|
|
} else if (button == 5) {
|
|
audio_set_volume(audio_state.volume - 5);
|
|
if (volume_slider != NULL && volume_slider->visible) {
|
|
volume_slider_render(volume_slider);
|
|
}
|
|
panel_render_all();
|
|
}
|
|
}
|
|
|
|
(void)y;
|
|
}
|
|
|
|
void systray_update(void)
|
|
{
|
|
|
|
if (wifi_menu != NULL && wifi_menu->visible) {
|
|
long now = get_time_ms();
|
|
pthread_mutex_lock(&state_mutex);
|
|
long last_scan = wifi_state.last_scan;
|
|
pthread_mutex_unlock(&state_mutex);
|
|
|
|
if (now - last_scan >= WIFI_SCAN_INTERVAL) {
|
|
wifi_scan_networks();
|
|
dropdown_render(wifi_menu);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void systray_lock(void)
|
|
{
|
|
pthread_mutex_lock(&state_mutex);
|
|
}
|
|
|
|
void systray_unlock(void)
|
|
{
|
|
pthread_mutex_unlock(&state_mutex);
|
|
}
|
|
|
|
BatteryState systray_get_battery_snapshot(void)
|
|
{
|
|
BatteryState snapshot;
|
|
pthread_mutex_lock(&state_mutex);
|
|
snapshot = battery_state;
|
|
pthread_mutex_unlock(&state_mutex);
|
|
return snapshot;
|
|
}
|
|
|
|
AudioState systray_get_audio_snapshot(void)
|
|
{
|
|
AudioState snapshot;
|
|
pthread_mutex_lock(&state_mutex);
|
|
snapshot = audio_state;
|
|
pthread_mutex_unlock(&state_mutex);
|
|
return snapshot;
|
|
}
|