Nimbin[12]?Sway / SwBr / commits / 43306f0

SwBr git · main

SwBr - status bar for sway

sway wayland c bar · first commit 2026-08-27 · last commit 2026-10-05 (4 days ago) · synced 3 days ago · upstream: github.com/nimbin2/SwBr

C 95.9% Markdown 3.7%
git clone https://git.christianimmanuel.de/sway/SwBr.gitwget https://git.christianimmanuel.de/sway/SwBr/archive/SwBr.tar.gz

Fix hover stuff

Christian Immanuel · 2026-08-30 02:06 · 43306f03ad7f25ae9f681c970f8578300c430079

 README.md      |  34 ++++++-
 config.example |  30 ++++--
 swbr.c         | 314 ++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 3 files changed, 339 insertions(+), 39 deletions(-)

diff --git a/README.md b/README.md
index 6e96320..08337df 100644
--- a/README.md
+++ b/README.md
@@ -109,7 +109,9 @@ already knows.
 
 `slim_align=1` (the default) gives each mark its cell's whole place — same left
 edge, same width — so folding changes the height of the bar and nothing else.
-`slim_align=0` packs them against the right edge instead.
+Starting folded, the open layout is worked out once with nothing drawn, so the
+marks are the right size from the first frame. `slim_align=0` packs them
+against the right edge instead.
 
 `signals=0` leaves a plain strip.
 
@@ -119,12 +121,34 @@ edge, same width — so folding changes the height of the bar and nothing else.
 placeholders its source understands, or `%s` for a command's output:
 
 ```
-battery.hover=%c%i  %w  %h     # 83-  21.5W  1.7h
-battery.hover_slim=1
+battery.hover=%w  %h           # 21.5W  1.7h — the cell already says 83-
+battery.hover_slim=0
 ```
 
-The cell is measured with that text, so it grows to fit. Folded there is
-nowhere to put it, so `hover_slim=1` opens the bar while you rest on that cell.
+It slides down out of the bar and fades in over `anim_ms`, centred on the
+cell: square where the two meet, rounded on the far side, and always clear of
+the bar's own rounded corners at either end. The surface grows to make room and
+shrinks again when you leave. The padding either side of a cell counts as
+part of it, and the panel itself takes the pointer, so resting on it keeps it
+open.
+
+`NAME.hover_cal=1` puts this month in the panel, with today in the `hl`
+colour. It is built in rather than shelled out to `cal(1)`, so the columns line
+up whatever the font and resting on the clock spawns nothing. Weeks start on
+Monday.
+
+`NAME.hover_cmd=` is the general form: any command's output, run once when the
+hover starts rather than on a timer. The panel is as tall as the output has
+lines either way.
+
+```
+clock.hover_cal=1
+clock.hover_cmd=cal      # the same idea, if you prefer cal's own layout
+```
+
+Folded there is no room for a panel, so there is no hover unless
+`hover_slim=1`, which opens the bar while you rest on that cell. Off by
+default.
 
 ## Load per workspace
 
diff --git a/config.example b/config.example
index 45cdd7e..1712746 100644
--- a/config.example
+++ b/config.example
@@ -188,6 +188,11 @@ mpv.slim_color=3ea6ff  # its own colour, so you can tell the two apart
 cell=clock
 clock.cmd=date '+<span foreground="#00ffff">%H:%M</span>  %a %d.%m'
 clock.interval=5
+clock.hover_cal=1  # the panel: this month, with today in the hl colour.
+                   # Built in, so the columns line up whatever the font and
+                   # nothing is spawned. hover_cmd=cal would work too
+# clock.hover_cmd=cal  # the panel: this month, from cal(1). Run once when
+                   # the hover starts, not on a timer
 clock.slim=clock   # twelve dots for the hour, in the same cyan the clock
                    # uses in the full bar (taken from its own markup)
 clock.slim_color2=2f7f9f   # the minutes bar, and the morning hours. Every dot
@@ -216,13 +221,16 @@ temp.slim=bar      # a gauge too, but degrees are not percent:
 temp.slim_min=40   # so map 40..100 C onto the gauge
 temp.slim_max=100
 
-# --- battery: draw and remaining time, then capacity, joined ---------------
-cell=power
-power.cmd=b=/sys/class/power_supply/BAT0; grep -q Discharging $b/status || exit 0; awk -v e="$(cat $b/energy_now)" -v p="$(cat $b/power_now)" 'BEGIN{if(p>0)printf "%.2fW %.1fh", p/1000000, e/p}'
-power.interval=10
-power.color=b3c0cd
-power.slim=off     # watts and hours say nothing as a bar
-power.sep=0
+# --- battery ---------------------------------------------------------------
+# The draw and the time left live in the hover panel now, so they are there
+# when you want them and not taking width when you do not. Uncomment this if
+# you would rather have them in the bar all the time.
+# cell=power
+# power.source=battery
+# power.src_fmt=%w %h
+# power.color=b3c0cd
+# power.slim=off
+# power.sep=0
 
 # The marker at the end tells the folded strip which way it is going:
 # + charging, - discharging, = full. In the full bar it reads as ⚡ or nothing.
@@ -236,8 +244,10 @@ battery.source=battery  # reads /sys itself: 83+ filling, 83- draining, no
                    # left, %s the word. This one reads "83- 21.5W 1.7h"
 # battery.cmd=...   # setting a command switches the cell back to your own
                    # script; source= and cmd= are the two ways, never both
-battery.hover=%c%i  %w  %h   # while the pointer is on it: 83-  21.5W  1.7h
-battery.hover_slim=1    # folded, resting on it opens the bar so that fits
+battery.hover=%w   %h   # the panel: just what the cell does not already
+                        # say — 21.5W  1.7h
+battery.hover_slim=0    # 1 = folded, resting on it opens the bar so the
+                        # panel has somewhere to go. 0 = no hover when folded
 battery.interval=10
 battery.color=e1eeff
 battery.warn=<30:cecb00
@@ -284,7 +294,7 @@ button7=cmus_control next
 #   (..) glues items together with no separator between them.
 #   names are cell names plus the built-ins "workspaces" and "mode".
 # ---------------------------------------------------------------------------
-bar={workspaces,mode||(cmus,cmusui),volume,(docker,mpv),clock,(cpu,temp),(power,battery)}
+bar={workspaces,mode||(cmus,cmusui),volume,(docker,mpv),clock,(cpu,temp),battery}
 
 # --- colors: #rrggbb or #rrggbbaa ------------------------------------------
 bg=26313fe6         # the bar itself
diff --git a/swbr.c b/swbr.c
index 04af404..ef53ea6 100644
--- a/swbr.c
+++ b/swbr.c
@@ -1071,6 +1071,8 @@ typedef struct {
     char  src_fmt[128];          /* how a known player is laid out           */
     char  src_path[256];         /* where to read it from, when that varies  */
     char  hover_fmt[192];        /* what to show while the pointer is on it  */
+    char  hover_cmd[256];        /* ...or the output of this, run on hover   */
+    int   hover_cal;             /* ...or a month, with today marked         */
     char  out_hover[512];        /* that, already filled in                  */
     int   hover_slim;            /* folded, hovering it opens the bar        */
     int   state;                 /* what that player is doing: PS_*          */
@@ -1302,6 +1304,8 @@ static void cell_set(Config *c, const char *name, const char *k, const char *v)
              !strcmp(k, "program"))
         str_set(e->src_path, sizeof(e->src_path), v);
     else if (!strcmp(k, "hover")) str_set(e->hover_fmt, sizeof(e->hover_fmt), v);
+    else if (!strcmp(k, "hover_cmd")) str_set(e->hover_cmd, sizeof(e->hover_cmd), v);
+    else if (!strcmp(k, "hover_cal")) e->hover_cal = atoi(v);
     else if (!strcmp(k, "hover_slim")) e->hover_slim = atoi(v);
     else if (!strcmp(k, "source")) {
         if (!strcasecmp(v, "cmus")) {
@@ -2849,19 +2853,12 @@ static bool cell_visible(Cell *e)
 }
 
 /* what a cell draws right now, and in which colour */
-static int HOVER_CELL = -1;      /* set per bar before it is measured */
 
 static void cell_display(Cell *e, char *out, size_t cap, Col *col)
 {
     if (cell_is_msg(e)) { str_set(out, cap, msg_text); *col = msg_color(); return; }
 
-    /* The pointer is on it and it has something longer to say. The cell is
-     * measured with this text too, so it grows to fit rather than truncating. */
-    if (HOVER_CELL >= 0 && e == &cfg.cell[HOVER_CELL] && e->out_hover[0]) {
-        str_set(out, cap, e->out_hover);
-        *col = cell_color(e);
-        return;
-    }
+
     if (!*e->out && *e->empty) { str_set(out, cap, e->empty); *col = cell_color(e); return; }
     cell_text(e, out, cap);
     *col = cell_color(e);
@@ -2914,6 +2911,12 @@ struct Bar {
     struct { float x0, x1; char name[128]; } hit[MAX_WORKSPACES];
     int hits;
     bool hover_open;            /* opened by a hover, folds back on leave */
+    bool measured_full;         /* the open layout has been worked out once */
+    int  tip_showing;           /* the cell whose panel is open, or -1      */
+    float tip_cx;               /* the middle of that cell, logical px      */
+    uint32_t tip_at;            /* when it opened, for the slide            */
+    int  tip_h;                 /* extra surface for the hover text, logical */
+    int  tip_cell;              /* which cell it belongs to, or -1          */
     float cellx0[MAX_CELLS], cellx1[MAX_CELLS];  /* where each cell sat in the
                                                     full bar, so the folded
                                                     strip can line up with it */
@@ -3957,6 +3960,138 @@ static int bar_natural_width(Bar *b, Canvas *cv, Font *f, float vis, float s)
     return want;
 }
 
+/* How tall the surface has to be: the bar, plus a strip for the hover text
+ * when there is one. Growing the cell instead pushed everything beside it
+ * along and left a hole at the end of the row, which is not what a tooltip
+ * should do to a bar. */
+static int bar_wanted_h(Bar *b)
+{
+    return bar_height_logical() + b->tip_h;
+}
+
+/* one row per line, so `cal` gets its six weeks */
+static int bar_tip_height(Bar *b, const char *txt)
+{
+    (void)b;
+    int lines = 1;
+    for (const char *p = txt; *p; ++p) if (*p == '\n') lines++;
+    int row = (int)lroundf(cfg.text_px * cfg.ui_scale * 1.35f);
+    if (row < 8) row = 8;
+    return lines * row + 8;
+}
+
+static void bar_fit_height(Bar *b)
+{
+    if (!b->ls) return;
+    int want = bar_wanted_h(b);
+    if (want == b->h) return;
+    ls_set_size(b->ls, (uint32_t)(cfg.min_width > 0 ? b->want_w : 0), (uint32_t)want);
+    wl_surface_commit(b->surf);
+}
+
+/* the text a hovered cell wants to show, or NULL */
+/* A month grid, built here rather than shelled out to cal(1): that way today
+ * can be marked, the columns line up whatever the font, and resting on the
+ * clock spawns nothing. Weeks start on Monday. */
+static void hover_cal_build(Cell *e)
+{
+    time_t now = time(NULL);
+    struct tm t;
+    localtime_r(&now, &t);
+
+    struct tm first = t;
+    first.tm_mday = 1;
+    first.tm_hour = 12; first.tm_min = 0; first.tm_sec = 0;
+    time_t ft = mktime(&first);
+    localtime_r(&ft, &first);
+
+    static const int LEN[] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
+    int year = first.tm_year + 1900, mon = first.tm_mon;
+    int days = LEN[mon];
+    if (mon == 1 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) days = 29;
+
+    int lead = (first.tm_wday + 6) % 7;          /* Monday first */
+    static const char *MON[] = { "January","February","March","April","May",
+                                 "June","July","August","September","October",
+                                 "November","December" };
+
+    char out[1024];
+    size_t o = 0;
+    o += (size_t)snprintf(out + o, sizeof(out) - o, "%s %d\n", MON[mon], year);
+    o += (size_t)snprintf(out + o, sizeof(out) - o, "Mo Tu We Th Fr Sa Su\n");
+
+    char hl[16];
+    snprintf(hl, sizeof(hl), "%02x%02x%02x", cfg.hl.r, cfg.hl.g, cfg.hl.b);
+
+    int col = 0;
+    for (int i = 0; i < lead; ++i, ++col)
+        o += (size_t)snprintf(out + o, sizeof(out) - o, "   ");
+    for (int d = 1; d <= days && o < sizeof(out) - 64; ++d) {
+        if (d == t.tm_mday)
+            o += (size_t)snprintf(out + o, sizeof(out) - o,
+                                  "<span foreground=\"%s\">%2d</span>%s", hl, d,
+                                  col == 6 ? "" : " ");
+        else
+            o += (size_t)snprintf(out + o, sizeof(out) - o, "%2d%s", d,
+                                  col == 6 ? "" : " ");
+        if (++col == 7) { col = 0; if (d < days) o += (size_t)snprintf(out + o, sizeof(out) - o, "\n"); }
+    }
+    str_set(e->out_hover, sizeof(e->out_hover), out);
+}
+
+/* `cal`, or anything else whose output is the panel. Run once when the hover
+ * starts and kept until it ends, so resting on the clock does not spawn a
+ * process every frame. */
+static void hover_cmd_run(Cell *e)
+{
+    e->out_hover[0] = 0;
+    FILE *p = popen(e->hover_cmd, "r");
+    if (!p) return;
+    size_t n = fread(e->out_hover, 1, sizeof(e->out_hover) - 1, p);
+    pclose(p);
+    e->out_hover[n] = 0;
+    while (n && (e->out_hover[n - 1] == '\n' || e->out_hover[n - 1] == ' '))
+        e->out_hover[--n] = 0;
+}
+
+static const char *bar_tip_text(Bar *b)
+{
+    if (!b->hovered) return NULL;
+
+    /* The panel is part of the bar's surface, so the pointer can be on it
+     * rather than on the cell — keep it open while it is. */
+    if (b->tip_showing >= 0 && b->tip_h > 0) {
+        bool top = strcmp(cfg.position, "bottom") != 0;
+        double t0 = top ? bar_visible_logical(b) : 0;
+        double t1 = top ? (double)b->h : (double)b->tip_h;
+        if (b->py >= t0 && b->py < t1) {
+            b->tip_cell = b->tip_showing;
+            return cfg.cell[b->tip_cell].out_hover;
+        }
+    }
+
+    /* half the gap either side counts as the cell: the padding between two
+     * cells belongs to whichever you are nearest, not to neither */
+    float slack = (float)cfg.cell_gap * 0.5f;
+    for (int i = 0; i < b->chits; ++i) {
+        if (b->px < b->chit[i].x0 - slack || b->px >= b->chit[i].x1 + slack) continue;
+        Cell *e = &cfg.cell[b->chit[i].cell];
+        if (!e->out_hover[0] && !*e->hover_cmd && !e->hover_cal) return NULL;
+        if (b->tip_showing != b->chit[i].cell) {
+            if (e->hover_cal)        hover_cal_build(e);
+            else if (*e->hover_cmd)  hover_cmd_run(e);
+        }
+        if (!e->out_hover[0]) return NULL;
+        b->tip_cell = b->chit[i].cell;
+        /* where the cell is, taken now: the hit areas are cleared before the
+         * panel is painted, and falling back to the pointer made the panel
+         * slide about under the cursor */
+        b->tip_cx = (b->chit[i].x0 + b->chit[i].x1) * 0.5f;
+        return e->out_hover;
+    }
+    return NULL;
+}
+
 static void bar_fit_width(Bar *b, Canvas *cv, Font *f, float vis, float s)
 {
     if (cfg.min_width <= 0 || !b->ls) return;
@@ -3970,11 +4105,116 @@ static void bar_fit_width(Bar *b, Canvas *cv, Font *f, float vis, float s)
 
 /* Everything the bar paints, into a plain canvas. Split out of bar_render so
  * it can be exercised without a compositor. */
+/* the hover text, in the strip of surface reserved under (or over) the bar */
+static void bar_paint_tip(Bar *b, Canvas *cv, Font *f, float s, bool top,
+                          float bar_y0, float bar_vis)
+{
+    const char *txt = b->tip_cell >= 0 && b->tip_cell < cfg.cell_count
+                    ? cfg.cell[b->tip_cell].out_hover : NULL;
+    if (!txt || !*txt || b->tip_h <= 0) return;
+
+    float th = (float)b->tip_h * s;
+    float ty = top ? bar_y0 + bar_vis : bar_y0 - th;
+
+    /* The canvas is clipped to the bar's own rows by the time we get here,
+     * and the panel is by definition outside them — so it was being drawn
+     * and thrown away every frame. Open the clip to its strip, and put it
+     * back for whatever the bar draws next. */
+    canvas_clip(cv, 0, (int)floorf(ty), cv->w, (int)ceilf(ty + th));
+
+    /* one Runs per line: `cal` is a block of text, not a sentence */
+    char buf[1024];
+    str_set(buf, sizeof(buf), txt);
+
+    char *line[24];
+    int   nlines = 0;
+    for (char *p = buf; *p && nlines < (int)(sizeof line / sizeof *line); ) {
+        line[nlines++] = p;
+        char *nl = strchr(p, '\n');
+        if (!nl) break;
+        *nl = 0;
+        p = nl + 1;
+    }
+
+    Runs rs[24];
+    int saved = cfg.markup;
+    cfg.markup = cfg.cell[b->tip_cell].markup;
+    float tw = 0;
+    for (int i = 0; i < nlines; ++i) {
+        markup_parse(line[i], &rs[i], cell_color(&cfg.cell[b->tip_cell]));
+        float lw = 0;
+        for (int j = 0; j < rs[i].n; ++j) lw += text_width(f, rs[i].v[j].text);
+        if (lw > tw) tw = lw;
+    }
+    cfg.markup = saved;
+
+    float pad = 8.0f * s;
+    float pw  = tw + 2.0f * pad;
+    float rad = clampf(cfg.radius * s, 0.0f, th * 0.5f);
+
+    /* Clear of the bar's rounded corners at either end, so the panel never
+       overlaps the curve. */
+    float edge = rad + 4.0f * s;
+    float bx = clampf(b->tip_cx * s - pw * 0.5f,
+                      edge, (float)cv->w - pw - edge);
+    if (bx < 0.0f) bx = 0.0f;
+
+    /* Slide out and fade in: it should arrive rather than appear. */
+    float k = cfg.anim_ms > 0
+            ? clampf((float)(now_ms() - b->tip_at) / (float)cfg.anim_ms, 0.0f, 1.0f)
+            : 1.0f;
+    float e = k * k * (3.0f - 2.0f * k);
+    float slide = (1.0f - e) * th * 0.45f;
+    ty += top ? -slide : slide;
+
+    if (top) fill_round(cv, bx, ty, pw, th, 0, 0, rad, rad,
+                        col_scale_alpha(cfg.bg, e));
+    else     fill_round(cv, bx, ty, pw, th, rad, rad, 0, 0,
+                        col_scale_alpha(cfg.bg, e));
+
+    float row = (th - 8.0f * s) / (float)(nlines > 0 ? nlines : 1);
+    for (int i = 0; i < nlines; ++i) {
+        float ly = ty + 4.0f * s + (float)i * row;
+        float base = baseline_for(f, ly, row, s);
+        float x = bx + pad;
+        for (int j = 0; j < rs[i].n; ++j) {
+            Col tc = rs[i].v[j].has_fg ? rs[i].v[j].fg : cfg.text;
+            text_draw(cv, f, x, base, col_scale_alpha(tc, e), rs[i].v[j].text);
+            x += text_width(f, rs[i].v[j].text);
+        }
+    }
+
+    canvas_clip(cv, 0, (int)floorf(bar_y0), cv->w, (int)ceilf(bar_y0 + bar_vis));
+}
+
+/* the three groups, laid out and drawn. With the clip closed it draws
+ * nothing and only records where things would go. */
+static void bar_measure_layout(Canvas *cv, Bar *b, Font *f, float y0,
+                               float vis, float s, float bw)
+{
+    if (!cfg.cell_count && !layout_n[G_LEFT]) return;
+    float gap = (float)cfg.cell_gap * s;
+    float left_limit = (float)cfg.pad_x * s;
+    float right = bw - (float)(cfg.pad_right >= 0 ? cfg.pad_right : cfg.pad_x) * s;
+
+    float lw = group_width(cv, b, f, G_LEFT, vis, s);
+    float after_left = left_limit + (lw > 0 ? lw + gap : 0.0f);
+    float rx = draw_group(cv, b, f, G_RIGHT, y0, vis, s, 1.0f, after_left, right);
+    float mx = draw_message(cv, b, f, y0, vis, s, 1.0f, after_left, rx - gap);
+    float cx = draw_group(cv, b, f, G_CENTER, y0, vis, s, 1.0f, after_left,
+                          fminf(rx, mx) - gap);
+    draw_group(cv, b, f, G_LEFT, y0, vis, s, 1.0f, left_limit, cx - gap);
+}
+
 static void bar_paint(Bar *b, Canvas *cv, float visL, float s)
 {
     bool top = strcmp(cfg.position, "bottom") != 0;
     float vis = visL * s;
+    float tip = (float)b->tip_h * s;
+    /* the bar stays flush with the screen edge; the tip takes the strip on
+       the inward side of it */
     float y0 = top ? 0.0f : (float)cv->h - vis;
+    (void)tip;
     float rad = cfg.radius * s;
     float bw = (float)cv->w;
 
@@ -3990,28 +4230,45 @@ static void bar_paint(Bar *b, Canvas *cv, float visL, float s)
 
     float fade = clampf(b->vis * 1.6f - 0.25f, 0.0f, 1.0f);
 
-    /* which cell the pointer is on, from the frame before — a pointer-move
-       behind at worst, and the cell has to be measured with its hover text
-       so it grows to fit instead of truncating */
-    HOVER_CELL = -1;
-    if (b->hovered)
-        for (int i = 0; i < b->chits; ++i)
-            if (b->px >= b->chit[i].x0 && b->px < b->chit[i].x1) {
-                HOVER_CELL = b->chit[i].cell;
-                break;
-            }
+    /* Which cell is under the pointer, and what it wants to say. It has to be
+     * worked out here, before the hit areas are cleared for this frame —
+     * asking afterwards found an empty list every time, which is why the
+     * panel never appeared. */
+    b->tip_cell = -1;
+    const char *tiptxt = fade > 0.01f ? bar_tip_text(b) : NULL;
+    int want_tip = tiptxt ? bar_tip_height(b, tiptxt) : 0;
+    if (want_tip != b->tip_h) { b->tip_h = want_tip; bar_fit_height(b); }
+    if (tiptxt && b->tip_showing != b->tip_cell) b->tip_at = now_ms();
+    b->tip_showing = tiptxt ? b->tip_cell : -1;
+
+    Font *f = font_get((int)lroundf(cfg.text_px * cfg.ui_scale * s));
 
     b->hits = 0;
     b->chits = 0;
     if (fade <= 0.01f) {
+        /* Folded before the bar has ever been drawn open, the strip has no
+         * positions to line up with. Run the full layout with the canvas
+         * clipped to nothing: it draws no pixels and records where every cell
+         * and workspace would sit. */
+        if (!b->measured_full) {
+            int sx0 = cv->cx0, sy0 = cv->cy0, sx1 = cv->cx1, sy1 = cv->cy1;
+            canvas_clip(cv, 0, 0, 0, 0);
+            bar_measure_layout(cv, b, f, y0, vis, s, bw);
+            cv->cx0 = sx0; cv->cy0 = sy0; cv->cx1 = sx1; cv->cy1 = sy1;
+            b->measured_full = true;
+            b->hits = 0;
+            b->chits = 0;
+        }
         draw_signals(cv, b, y0, vis, s, &rs);
         return;
     }
+    b->measured_full = true;
 
     canvas_clip(cv, 0, (int)floorf(y0), (int)bw, (int)ceilf(y0 + vis));
 
-    Font *f = font_get((int)lroundf(cfg.text_px * cfg.ui_scale * s));
     bar_fit_width(b, cv, f, vis, s);
+    bar_paint_tip(b, cv, f, s, top, y0, vis);
+
     float gap = (float)cfg.cell_gap * s;
     float left_limit = (float)cfg.pad_x * s;
     float right = bw - (float)(cfg.pad_right >= 0 ? cfg.pad_right : cfg.pad_x) * s;
@@ -4064,13 +4321,16 @@ static void bar_render(Bar *b)
     /* only the visible strip takes clicks — the rest of the surface is
      * transparent and must not swallow them */
     struct wl_region *reg = wl_compositor_create_region(compositor);
-    int rv = (int)ceilf(visL);
+    /* the bar, plus the panel while one is open: the pointer has to be able
+     * to rest on the panel without the hover ending under it */
+    int rv = (int)ceilf(visL) + (b->tip_h > 0 ? b->tip_h : 0);
     wl_region_add(reg, 0, top ? 0 : b->h - rv, b->w, rv);
     wl_surface_set_input_region(b->surf, reg);
     wl_region_destroy(reg);
 
-    if (cfg.exclusive)
-        ls_set_exclusive_zone(b->ls, rv + cfg.margin);
+    if (cfg.exclusive)                       /* the panel must not reserve
+                                                space: it is transient */
+        ls_set_exclusive_zone(b->ls, (int)ceilf(visL) + cfg.margin);
 
     wl_surface_commit(b->surf);
     b->dirty = false;
@@ -4087,7 +4347,13 @@ static void bar_set_collapsed(Bar *b, bool c)
     b->dirty = true;
 }
 
-static bool bar_animating(Bar *b) { return b->vis != b->anim_to; }
+static bool bar_animating(Bar *b)
+{
+    if (b->vis != b->anim_to) return true;
+    if (b->tip_showing >= 0 && cfg.anim_ms > 0 &&
+        (int32_t)(now_ms() - b->tip_at) < cfg.anim_ms) return true;   /* sliding */
+    return false;
+}
 
 static void bar_anim_step(Bar *b, uint32_t t)
 {
@@ -4142,7 +4408,7 @@ static void bar_create(Output *o)
     b->out = o;
     b->scale = o->scale > 0 ? o->scale : 1;
     b->w = 0;
-    b->h = bar_height_logical();
+    b->h = bar_wanted_h(b);
     b->vis = cfg.start_collapsed ? 0.0f : 1.0f;
     b->anim_to = b->vis;
     b->collapsed = cfg.start_collapsed != 0;