From b393871e4b60ccfd7227dcca50591fbfc12267f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 13:35:52 +0000 Subject: [PATCH 1/2] feat: expand existing tray icons horizontally on click to reveal detail text Agent-Logs-Url: https://github.com/exynoxx/LumenShell/sessions/16b13b3a-cd46-433f-8fa4-8d15adabb3e9 Co-authored-by: exynoxx <5389754+exynoxx@users.noreply.github.com> --- Exy-panel/src/components/tray.vala | 63 +++++++++----------- Exy-panel/src/components/trays/battery.vala | 24 +++++--- Exy-panel/src/components/trays/trayicon.vala | 62 +++++++++++++++---- Exy-panel/src/components/trays/wifi.vala | 13 ++-- 4 files changed, 103 insertions(+), 59 deletions(-) diff --git a/Exy-panel/src/components/tray.vala b/Exy-panel/src/components/tray.vala index da47a18..4c569ff 100644 --- a/Exy-panel/src/components/tray.vala +++ b/Exy-panel/src/components/tray.vala @@ -23,7 +23,6 @@ public class Tray { private int screen_width; private ITray[] trays; - private int base_width; private int width; private int height; @@ -31,15 +30,11 @@ public class Tray { private int y; private bool hovered; - private Transition expand_animation; - public Tray(Context ctx, int screen_width){ this.ctx = ctx; this.screen_width = screen_width; - this.y = TRAY_Y; this.height = TRAY_HEIGHT; - //calc width var wifi = new WifiTray(); var battery = new BatteryTray(); var clock = new Clock(ctx); @@ -50,22 +45,8 @@ public class Tray { trays += clock; trays += exit; - foreach(var t in trays) - base_width += t.get_width(); - - base_width+=4*SPACING; - width = base_width; - - //calc x's - this.x = screen_width - width - MARGIN_RIGHT; - - var current_x = this.x + SPACING; - foreach (var tray in trays){ - tray.set_position(current_x, TRAY_Y); - current_x += tray.get_width() + SPACING; - } - - expand_animation = new TransitionEmpty(); + // Initial layout (recalculate() will keep it up to date every frame). + recalculate(); } @@ -102,31 +83,41 @@ public class Tray { } private void expand(){ - expand_animation = new Transition1D(0, &width, 400, 1d); - var height_animation = new Transition1D(1, &height, TRAY_MAX_HEIGHT, 1d); - animations.add(expand_animation); - animations.add(height_animation); + animations.add(new Transition1D(1, &height, TRAY_MAX_HEIGHT, 1d)); } private void contract(){ - expand_animation = new Transition1D(0, &width, base_width, 1d); - var height_animation = new Transition1D(1, &height, TRAY_HEIGHT, 1d); - animations.add(expand_animation); - animations.add(height_animation); + animations.add(new Transition1D(1, &height, TRAY_HEIGHT, 1d)); } public void on_mouse_leave(){ - if(width > base_width){ - contract(); - } + contract(); redraw = true; } - public void render(){ - if(!expand_animation.finished){ - this.x = screen_width - width - MARGIN_RIGHT; - this.y = HEIGHT - height - MARGIN_TOP; + // Recompute total width from current item widths and reposition all items. + // Called every render frame so item expansion and contraction are reflected instantly. + private void recalculate(){ + int total = 0; + foreach(var t in trays) + total += t.get_width(); + total += trays.length * SPACING; + width = total; + + // Right-align the tray; left edge moves left as items expand. + x = screen_width - width - MARGIN_RIGHT; + // Tray container top; icons are always pinned to TRAY_Y. + y = HEIGHT - height - MARGIN_TOP; + + var current_x = x + SPACING; + foreach(var t in trays){ + t.set_position(current_x, TRAY_Y); + current_x += t.get_width() + SPACING; } + } + + public void render(){ + recalculate(); ctx.draw_rect_rounded(this.x, this.y, width, height, 24, {0.15f,0.15f,0.15f,1}); foreach(var t in trays){ diff --git a/Exy-panel/src/components/trays/battery.vala b/Exy-panel/src/components/trays/battery.vala index 9b012ad..e74b266 100644 --- a/Exy-panel/src/components/trays/battery.vala +++ b/Exy-panel/src/components/trays/battery.vala @@ -8,27 +8,31 @@ voltage_now, current_now → detailed info */ public class BatteryTray : TrayIcon { + private int bat_percent = -1; + private string bat_status_str = ""; + public BatteryTray() { base ("nobattery"); status(); } - public override void mouse_down(){ - } - public override void mouse_up(){ - + protected override string get_detail_text() { + if(bat_percent < 0) return ""; + return "%d%% (%s)".printf(bat_percent, bat_status_str); } private async void status(){ - var status = exec("cat /sys/class/power_supply/BAT0/status"); + var st = exec("cat /sys/class/power_supply/BAT0/status"); var new_icon = "nobattery"; - if(status == "discharging" || status.contains("full")){ + if(st == "discharging" || st.contains("full")){ var full = exec_int("cat /sys/class/power_supply/BAT0/charge_full"); var current = exec_int("cat /sys/class/power_supply/BAT0/charge_now"); var percent = (current/(float)full)*100; + bat_percent = (int)percent; + bat_status_str = st.contains("full") ? "Full" : "Discharging"; print("Battery: %f\n", percent); if(percent >= 70) @@ -38,10 +42,14 @@ public class BatteryTray : TrayIcon { else new_icon = "mid"; - } else if (status == "charging"){ + } else if (st == "charging"){ new_icon = "charging"; + bat_status_str = "Charging"; + var full = exec_int("cat /sys/class/power_supply/BAT0/charge_full"); + var current = exec_int("cat /sys/class/power_supply/BAT0/charge_now"); + bat_percent = (int)((current/(float)full)*100); } else { - print("battery: status unknown: >%s<\n", status); + print("battery: status unknown: >%s<\n", st); return; } diff --git a/Exy-panel/src/components/trays/trayicon.vala b/Exy-panel/src/components/trays/trayicon.vala index 4370533..59e575a 100644 --- a/Exy-panel/src/components/trays/trayicon.vala +++ b/Exy-panel/src/components/trays/trayicon.vala @@ -15,25 +15,35 @@ public abstract class TrayIcon : Object, ITray { private const string base_path = "/home/nicholas/Dokumenter/layer-shell-experiments/Exy-panel/src/res/"; private const int ICON_SIZE = 32; private const int HOVER_RADIUS = 24; + private const int COLLAPSED_WIDTH = HOVER_RADIUS * 2; + private const int EXPANDED_WIDTH = 180; private const int MARGIN_TOP = (Tray.TRAY_HEIGHT - ICON_SIZE)/2; + // Each TrayIcon instance gets a unique animation slot ID. + // Start at 100 to avoid collisions with Tray's own animation IDs (0, 1). + private static int anim_id_counter = 100; + private int anim_id; + protected GLuint tex; protected bool hovered; - + protected bool expanded = false; + private int x; private int y; private int circle_x; private int circle_y; - private int width; + // Animated width: starts collapsed, grows to EXPANDED_WIDTH on click. + private int current_width; protected TrayIcon(string icon){ load(icon); - width = HOVER_RADIUS*2; + current_width = COLLAPSED_WIDTH; + anim_id = anim_id_counter++; } public int get_width() { - return width; + return current_width; } public void set_position(int x, int y){ @@ -60,6 +70,12 @@ public abstract class TrayIcon : Object, ITray { DrawKit.texture_free(tex); } + // Returns the detail string shown when this icon is expanded. + // Subclasses override to provide meaningful text; empty string disables expansion. + protected virtual string get_detail_text() { + return ""; + } + public virtual void mouse_motion(int mouse_x, int mouse_y){ var hover_initial = hovered; @@ -73,8 +89,18 @@ public abstract class TrayIcon : Object, ITray { redraw = true; } - public abstract void mouse_down(); - public abstract void mouse_up(); + // Default: toggle expansion when clicked while hovered (if detail text is available). + // Subclasses that need different behaviour should override without calling base. + public virtual void mouse_down(){ + if(hovered && get_detail_text() != "") { + expanded = !expanded; + int target = expanded ? EXPANDED_WIDTH : COLLAPSED_WIDTH; + animations.add(new Transition1D(anim_id, ¤t_width, target, 0.4)); + redraw = true; + } + } + + public virtual void mouse_up(){} public virtual void render(Context ctx){ @@ -83,10 +109,26 @@ public abstract class TrayIcon : Object, ITray { ctx.set_tex_color({0,0,0,1}); ctx.draw_texture(tex, x, y, ICON_SIZE, ICON_SIZE); ctx.set_tex_color({1,1,1,1}); - return; - - } + } else { + ctx.draw_texture(tex, x, y, ICON_SIZE, ICON_SIZE); + } - ctx.draw_texture(tex, x, y, ICON_SIZE, ICON_SIZE); + // Draw detail text to the right of the icon as the slot expands. + // Wait for a small amount of expansion before drawing to avoid a one-frame flash. + if(current_width > COLLAPSED_WIDTH + 4) { + float progress = float.min( + (float)(current_width - COLLAPSED_WIDTH) / (float)(EXPANDED_WIDTH - COLLAPSED_WIDTH), + 1.0f); + + string detail = get_detail_text(); + if(detail != "") { + // Centre the text in the expanded area to the right of the icon. + int text_area_left = x + ICON_SIZE + 8; + int text_area_width = current_width - ICON_SIZE - 8; + int text_center_x = text_area_left + text_area_width / 2; + int text_y = y + ICON_SIZE / 2 + 4; + ctx.draw_text(detail, text_center_x, text_y, 13, {1, 1, 1, progress}); + } + } } } \ No newline at end of file diff --git a/Exy-panel/src/components/trays/wifi.vala b/Exy-panel/src/components/trays/wifi.vala index 845d0f7..a6a28b4 100644 --- a/Exy-panel/src/components/trays/wifi.vala +++ b/Exy-panel/src/components/trays/wifi.vala @@ -23,16 +23,15 @@ public class WifiTray : TrayIcon { } } + private string wifi_connection = ""; + public WifiTray() { base ("wifi-unknown"); status(); } - public override void mouse_down(){ - - } - public override void mouse_up(){ - + protected override string get_detail_text() { + return wifi_connection; } private async void status(){ @@ -46,9 +45,13 @@ public class WifiTray : TrayIcon { foreach(var endpoint in endpoints){ if(endpoint.state == "connected"){ wifi_connected = true; + wifi_connection = endpoint.connection != "--" ? endpoint.connection : endpoint.device; } } + if(!wifi_connected) + wifi_connection = "Not Connected"; + var new_icon = (wifi_connected) ? "wifi" : "nowifi"; free(); From 13ab96391f5a2450fe2158929b9e14245fc9d289 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 13:44:58 +0000 Subject: [PATCH 2/2] feat: ExitTray expands to show Close App / Shutdown prompt Agent-Logs-Url: https://github.com/exynoxx/LumenShell/sessions/23b0313f-41ef-410d-b49a-4d5551bf796e Co-authored-by: exynoxx <5389754+exynoxx@users.noreply.github.com> --- Exy-panel/src/components/trays/exit.vala | 114 ++++++++++++++++++- Exy-panel/src/components/trays/trayicon.vala | 54 +++++---- 2 files changed, 142 insertions(+), 26 deletions(-) diff --git a/Exy-panel/src/components/trays/exit.vala b/Exy-panel/src/components/trays/exit.vala index d95f797..05d0a82 100644 --- a/Exy-panel/src/components/trays/exit.vala +++ b/Exy-panel/src/components/trays/exit.vala @@ -1,15 +1,117 @@ public class ExitTray : TrayIcon { + // Wider slot to fit two labelled option buttons side-by-side. + private const int EXIT_EXPANDED_WIDTH = 260; + // Pixel offset where the option area begins (past the icon + a small gap). + private const int OPT_OFFSET = ICON_SIZE + 8; + + // Which option area the cursor is currently over: 0=none, 1=Close App, 2=Shutdown + private int option_hovered = 0; + public ExitTray() { - base ("close"); + base("close"); + } + + protected override int get_expanded_width() { + return EXIT_EXPANDED_WIDTH; + } + + // Non-empty return enables the base expand mechanics; the actual content is + // rendered by our render() override, so this text is never drawn directly. + protected override string get_detail_text() { + return "exit"; } - public override void mouse_down(){ - if(base.hovered) - Process.spawn_command_line_async("pkill wayfire"); + public override void mouse_motion(int mouse_x, int mouse_y) { + base.mouse_motion(mouse_x, mouse_y); + + int prev = option_hovered; + option_hovered = 0; + + // Track which option button the cursor is over while the slot is open. + if (expanded && current_width > COLLAPSED_WIDTH + MIN_EXPAND_THRESHOLD) { + int opt_start = render_x + OPT_OFFSET; + int opt_total = current_width - OPT_OFFSET; + int half = opt_total / 2; + + if (mouse_x >= opt_start && mouse_x < opt_start + half) + option_hovered = 1; + else if (mouse_x >= opt_start + half && mouse_x < render_x + current_width) + option_hovered = 2; + } + + if (option_hovered != prev) + redraw = true; } - public override void mouse_up(){ + public override void mouse_down() { + if (!expanded) { + // Expand on icon click. + if (hovered) { + expanded = true; + animations.add(new Transition1D(anim_id, ¤t_width, EXIT_EXPANDED_WIDTH, 0.4)); + redraw = true; + } + } else { + if (option_hovered == 1) { + // Close the currently focused application. + try { + Process.spawn_command_line_async("wlrctl window focus kill"); + } catch (Error e) { + warning("ExitTray: close app failed: %s", e.message); + } + collapse(); + } else if (option_hovered == 2) { + // Shut down the system. + try { + Process.spawn_command_line_async("systemctl poweroff"); + } catch (Error e) { + warning("ExitTray: shutdown failed: %s", e.message); + } + } else if (hovered) { + // Clicking the icon while expanded collapses the prompt. + collapse(); + } + } + } + + public override void mouse_up() {} + + private void collapse() { + expanded = false; + option_hovered = 0; + animations.add(new Transition1D(anim_id, ¤t_width, COLLAPSED_WIDTH, 0.4)); + redraw = true; + } + + public override void render(Context ctx) { + render_icon(ctx); + + if (current_width > COLLAPSED_WIDTH + MIN_EXPAND_THRESHOLD) { + float progress = float.min( + (float)(current_width - COLLAPSED_WIDTH) / (float)(EXIT_EXPANDED_WIDTH - COLLAPSED_WIDTH), + 1.0f); + + int opt_start = render_x + OPT_OFFSET; + int opt_total = current_width - OPT_OFFSET; + int half = opt_total / 2; + int btn_h = ICON_SIZE + 4; + int text_y = render_y + ICON_SIZE / 2 + 4; + + // Option hover highlight backgrounds. + if (option_hovered == 1) { + ctx.draw_rect_rounded(opt_start, render_y - 2, half, btn_h, 8, + {0.3f, 0.3f, 0.3f, progress}); + } + if (option_hovered == 2) { + ctx.draw_rect_rounded(opt_start + half, render_y - 2, half, btn_h, 8, + {0.65f, 0.12f, 0.12f, progress}); + } + + // Centred labels for each option. + ctx.draw_text("Close App", opt_start + half / 2, text_y, 13, {1, 1, 1, progress}); + ctx.draw_text("Shutdown", opt_start + half + half / 2, text_y, 13, {1, 1, 1, progress}); + } } -} \ No newline at end of file +} diff --git a/Exy-panel/src/components/trays/trayicon.vala b/Exy-panel/src/components/trays/trayicon.vala index 59e575a..d2042be 100644 --- a/Exy-panel/src/components/trays/trayicon.vala +++ b/Exy-panel/src/components/trays/trayicon.vala @@ -13,28 +13,31 @@ public interface ITray : GLib.Object{ public abstract class TrayIcon : Object, ITray { private const string base_path = "/home/nicholas/Dokumenter/layer-shell-experiments/Exy-panel/src/res/"; - private const int ICON_SIZE = 32; + protected const int ICON_SIZE = 32; private const int HOVER_RADIUS = 24; - private const int COLLAPSED_WIDTH = HOVER_RADIUS * 2; + protected const int COLLAPSED_WIDTH = HOVER_RADIUS * 2; private const int EXPANDED_WIDTH = 180; + // Minimum expansion in pixels before detail content is drawn (avoids a one-frame flash). + protected const int MIN_EXPAND_THRESHOLD = 4; private const int MARGIN_TOP = (Tray.TRAY_HEIGHT - ICON_SIZE)/2; // Each TrayIcon instance gets a unique animation slot ID. // Start at 100 to avoid collisions with Tray's own animation IDs (0, 1). private static int anim_id_counter = 100; - private int anim_id; + protected int anim_id; protected GLuint tex; protected bool hovered; protected bool expanded = false; - private int x; - private int y; + // Adjusted drawing position (after MARGIN_TOP is applied). + protected int render_x; + protected int render_y; private int circle_x; private int circle_y; - // Animated width: starts collapsed, grows to EXPANDED_WIDTH on click. - private int current_width; + // Animated width: starts collapsed, grows to get_expanded_width() on click. + protected int current_width; protected TrayIcon(string icon){ load(icon); @@ -47,10 +50,16 @@ public abstract class TrayIcon : Object, ITray { } public void set_position(int x, int y){ - this.x = x; - this.y = y + MARGIN_TOP; - this.circle_x = this.x + ICON_SIZE/2; - this.circle_y = this.y + ICON_SIZE/2; + this.render_x = x; + this.render_y = y + MARGIN_TOP; + this.circle_x = render_x + ICON_SIZE/2; + this.circle_y = render_y + ICON_SIZE/2; + } + + // Returns the target width when this icon is expanded. + // Subclasses can override for a wider slot. + protected virtual int get_expanded_width() { + return EXPANDED_WIDTH; } public void load(string icon){ @@ -94,7 +103,7 @@ public abstract class TrayIcon : Object, ITray { public virtual void mouse_down(){ if(hovered && get_detail_text() != "") { expanded = !expanded; - int target = expanded ? EXPANDED_WIDTH : COLLAPSED_WIDTH; + int target = expanded ? get_expanded_width() : COLLAPSED_WIDTH; animations.add(new Transition1D(anim_id, ¤t_width, target, 0.4)); redraw = true; } @@ -102,31 +111,36 @@ public abstract class TrayIcon : Object, ITray { public virtual void mouse_up(){} - public virtual void render(Context ctx){ - + // Draw the icon and its hover highlight; available for subclasses that override render() + // but still want the standard icon appearance. + protected void render_icon(Context ctx){ if(hovered){ ctx.draw_circle(circle_x, circle_y, 24, {1,1,1,1}); ctx.set_tex_color({0,0,0,1}); - ctx.draw_texture(tex, x, y, ICON_SIZE, ICON_SIZE); + ctx.draw_texture(tex, render_x, render_y, ICON_SIZE, ICON_SIZE); ctx.set_tex_color({1,1,1,1}); } else { - ctx.draw_texture(tex, x, y, ICON_SIZE, ICON_SIZE); + ctx.draw_texture(tex, render_x, render_y, ICON_SIZE, ICON_SIZE); } + } + + public virtual void render(Context ctx){ + render_icon(ctx); // Draw detail text to the right of the icon as the slot expands. // Wait for a small amount of expansion before drawing to avoid a one-frame flash. - if(current_width > COLLAPSED_WIDTH + 4) { + if(current_width > COLLAPSED_WIDTH + MIN_EXPAND_THRESHOLD) { float progress = float.min( - (float)(current_width - COLLAPSED_WIDTH) / (float)(EXPANDED_WIDTH - COLLAPSED_WIDTH), + (float)(current_width - COLLAPSED_WIDTH) / (float)(get_expanded_width() - COLLAPSED_WIDTH), 1.0f); string detail = get_detail_text(); if(detail != "") { // Centre the text in the expanded area to the right of the icon. - int text_area_left = x + ICON_SIZE + 8; + int text_area_left = render_x + ICON_SIZE + 8; int text_area_width = current_width - ICON_SIZE - 8; int text_center_x = text_area_left + text_area_width / 2; - int text_y = y + ICON_SIZE / 2 + 4; + int text_y = render_y + ICON_SIZE / 2 + 4; ctx.draw_text(detail, text_center_x, text_y, 13, {1, 1, 1, progress}); } }