SWOv git · main
SWOv - window overview for sway
C 94% Markdown 5.4%git clone https://git.christianimmanuel.de/sway/SWOv.gitwget https://git.christianimmanuel.de/sway/SWOv/archive/SWOv.tar.gzswov: enter follows the keyboard, and the cursor waits to be asked
Two faults with one cause: the selection could be set by a pointer that merely happened to be resting somewhere, and nothing told that apart from a choice someone made. Two flags now do. sel_by_key — the keyboard put the selection there. Tab and the arrows pick exactly one thing, so enter acts on that and nothing else: a workspace when that is what was picked, which is how tabbing through hits and pressing enter is supposed to work. The previous release made enter re-ask the query whenever the selection was not itself a matching window, to stop a resting pointer from stealing the hit; tabbing to a workspace leaves no window picked and tripped exactly that condition, so enter threw away where you had tabbed to and jumped back to the first hit. Only an incidental selection goes back to the query now. mouse_moved — the pointer has actually moved since the window opened. Until then it selects nothing and no cursor is drawn, so opening the overview no longer lights up whatever the mouse was left on top of. A key, a click, a relayed pointer from swas, or a query showing its hit all bring the cursor out; start_selection still decides where the keyboard starts from. Hover had to stop keying off "what is under the pointer changed" for this. A new window is sent a motion event for wherever the pointer already is, so that position was recorded before movement counted, and moving inside the same tile afterwards changed nothing to notice — the cursor only appeared once the pointer crossed into a different workspace. Found by the test below, not by reading it. Checked against the real decision with a stand-in sway on a socketpair, ten cases: enter after typing a query, after tabbing once, twice and three times, onto an empty workspace, onto a workspace matched only by name, with a pointer resting somewhere nobody searched, with no query at all, and the cursor's visibility across a fresh open, a first motion event, a 1px jitter, a real move and a tab.
README.md | 8 +++- commit_message.txt | 51 ++++++++++++++----------- config.example | 3 ++ swov.c | 108 ++++++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 126 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 5a53144..fc81a94 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ does nothing at all — every key goes to whatever is underneath it. | middle click | close that window straight away | | `f` | find windows by title, app id or workspace name; the best hit is selected, nothing is while nothing matches | | `/` | filter: same search, but hides everything else | +| `tab` in a search | the next workspace with a hit; `enter` then goes there | | `r` | reload | | `esc` | cancel a drag, else quit | @@ -64,7 +65,12 @@ Mouse and keyboard share one cursor: pointing at a window selects it, so Click the pencil at the right end of a workspace header to rename it; `enter` keeps it, `esc` drops it. The rest of the header selects and drags like the tile. The name is what `f` and `/` search, next to app ids and titles. -swov opens with the current workspace selected as a whole, no window picked. +swov opens with the current workspace selected as a whole, no window picked — +but the cursor is not drawn until you point or type. The pointer is already +somewhere when the overview appears, and whatever it was left on top of is not +a choice anyone made, so it selects nothing until it moves. A key, a click, or +a query showing its hit brings the cursor out. + Orange is only ever the selection cursor; the workspace sway is showing and the window it has focused are marked in teal (`current`), search hits in violet (`match`). diff --git a/commit_message.txt b/commit_message.txt index 440191f..fc2706b 100644 --- a/commit_message.txt +++ b/commit_message.txt @@ -1,25 +1,34 @@ -swov: fix enter after a search, clicking a free number, and cropped SVG icons +swov: enter follows the keyboard, and the cursor waits to be asked -Enter during a search opened whatever the cursor happened to be on. Pointing -at anything selects it, so a pointer resting elsewhere — or merely passed over -on the way to the keyboard — replaced the hit the query had chosen, and enter -focused a workspace nobody searched for. Enter now checks that what is -selected is itself a match and asks the query again if it is not, falling back -to a workspace whose own name matched, and flashing the field when nothing -did. The best-hit pick moved out of apply_filter so it can be asked twice. +Two faults with one cause: the selection could be set by a pointer that merely +happened to be resting somewhere, and nothing told that apart from a choice +someone made. Two flags now do. -Clicking a free number did nothing because it quit instead. The hit test only -knows the workspaces sway actually has, so a free slot looked like a click on -the empty background, and the press handler closed the overview before the -release could act on it. A press on a slot now arms the click like any other. +sel_by_key — the keyboard put the selection there. Tab and the arrows pick +exactly one thing, so enter acts on that and nothing else: a workspace when +that is what was picked, which is how tabbing through hits and pressing enter +is supposed to work. The previous release made enter re-ask the query whenever +the selection was not itself a matching window, to stop a resting pointer from +stealing the hit; tabbing to a workspace leaves no window picked and tripped +exactly that condition, so enter threw away where you had tabbed to and jumped +back to the first hit. Only an incidental selection goes back to the query now. -An SVG icon whose drawing runs outside its declared width/height was cut off: -SDL_image rasterises to the box the file declares and simply does not draw -past it. nanosvg is vendored here now (the same copy swas uses) and the box is -the declared canvas and the shapes' own bounds together — an icon with -deliberate padding keeps it, one that overflows is scaled down instead of -losing an edge. SDL_image stays as the fallback. +mouse_moved — the pointer has actually moved since the window opened. Until +then it selects nothing and no cursor is drawn, so opening the overview no +longer lights up whatever the mouse was left on top of. A key, a click, a +relayed pointer from swas, or a query showing its hit all bring the cursor out; +start_selection still decides where the keyboard starts from. -Checked against a 48x48 icon whose art reaches 88x88: ink running into the -bottom and right edges drops from 54 pixels to 9, and well-formed icons -rasterise byte for byte as before. +Hover had to stop keying off "what is under the pointer changed" for this. A +new window is sent a motion event for wherever the pointer already is, so that +position was recorded before movement counted, and moving inside the same tile +afterwards changed nothing to notice — the cursor only appeared once the +pointer crossed into a different workspace. Found by the test below, not by +reading it. + +Checked against the real decision with a stand-in sway on a socketpair, ten +cases: enter after typing a query, after tabbing once, twice and three times, +onto an empty workspace, onto a workspace matched only by name, with a pointer +resting somewhere nobody searched, with no query at all, and the cursor's +visibility across a fresh open, a first motion event, a 1px jitter, a real +move and a tab. diff --git a/config.example b/config.example index 8c59495..1c894a3 100644 --- a/config.example +++ b/config.example @@ -126,6 +126,9 @@ start_selection=workspace # workspace = the visible workspace, no window (default) # none = nothing at all # window = the first window of the visible workspace + # Whichever it is, the cursor is not drawn until you point + # or type: it is where the keyboard starts from, not a + # highlight on something you did not choose all_outputs=0 # 1 = show every monitor, not just the focused one show_header=1 # the "DP-1 16 windows on 9 workspaces" line show_hints=1 # the key hints diff --git a/swov.c b/swov.c index c2e910c..8f35604 100644 --- a/swov.c +++ b/swov.c @@ -2789,6 +2789,25 @@ static float pos_x(const char *p, float w, float margin) static int sel_ws = 0; static bool sel_active = true; /* false: no tile and no window highlighted */ + +/* Where the selection came from, and whether the pointer has done anything + * at all. Pointing at something selects it, which is what makes space, x and + * the rest work under the mouse — but it means the selection can also be set + * by a pointer that merely happens to be resting somewhere, and that is not a + * choice anyone made. Two flags tell the two apart: + * sel_by_key the keyboard put it there: tab, the arrows, hjkl. Deliberate, + * so enter acts on exactly that, workspace or window. + * mouse_moved the pointer has actually moved since the window opened, so + * where it sits means something. Until then it selects nothing + * and no cursor is drawn: opening the overview must not light + * up whatever the mouse was left on top of. */ +static bool sel_by_key; /* false until the user does something */ +static bool mouse_moved; +static float mouse_x0, mouse_y0; +static bool mouse_seen; /* we have a position to compare against */ + +/* the selection cursor is only drawn once the user has pointed or typed */ +static bool sel_shown(void) { return sel_active && (sel_by_key || mouse_moved); } static int hov_ws = -1, hov_win = -1; static bool pencil_hot; /* the pointer is on a rename pencil */ static void draw_pencil(SDL_FRect r, SDL_FColor c); @@ -3471,6 +3490,7 @@ static void select_ws(int idx, bool keep_window); static void select_ws(int idx, bool keep_window) { if (NWS == 0) return; + sel_by_key = true; sel_ws = (idx % NWS + NWS) % NWS; Ws *ws = &WSS[sel_ws]; if (!keep_window || ws->count == 0) { ws->sel = -1; return; } @@ -3554,6 +3574,7 @@ static bool step_recent(int dir) if (w->ws < 0 || w->ws >= NWS) return false; sel_ws = w->ws; sel_active = true; + sel_by_key = true; WSS[sel_ws].sel = (int)(w - &WINS[WSS[sel_ws].first]); dirty = true; return true; @@ -3705,6 +3726,7 @@ static void apply_filter(void) sel_ws = WINS[best].ws; WSS[sel_ws].sel = best - WSS[sel_ws].first; sel_active = true; + sel_by_key = true; /* the query chose it, not the mouse */ return; } /* Nothing matched. Leaving the previous selection lit says something @@ -4522,7 +4544,7 @@ static void draw_card(Win *w, bool tile_selected) SDL_FRect r = xf(w->card); if (r.w < 4.0f || r.h < 4.0f) return; - bool selected = tile_selected && sel_active && ws_sel_win(&WSS[w->ws]) == w; + bool selected = tile_selected && sel_shown() && ws_sel_win(&WSS[w->ws]) == w; bool is_hovered = selected; /* pointer and keyboard share a cursor */ bool dimmed = filtering && !w->match; bool is_hit = searching && qlen > 0 && w->match; @@ -4817,7 +4839,7 @@ static SDL_FColor output_colour(const char *name); static void draw_workspace(int idx) { Ws *ws = &WSS[idx]; - bool selected = sel_active && (idx == sel_ws); + bool selected = sel_shown() && (idx == sel_ws); bool hovered = false; /* the selection is the only cursor */ float rad = C.radius * SC; @@ -6073,7 +6095,12 @@ static void handle_key(const SDL_KeyboardEvent *k) * hit, and enter opened a workspace nobody searched for. If what * is selected is not itself a match, the query is asked again. */ if (NWS == 0) { running = false; return; } - if (qlen > 0) { + /* Tab and the arrows pick exactly one thing, so enter acts on + * that and nothing else — a workspace if that is what was picked, + * which is how tabbing through hits onto an empty one works. Only + * an incidental selection, from a pointer resting on something + * nobody searched for, sends the question back to the query. */ + if (qlen > 0 && !(sel_active && sel_by_key)) { Win *w = sel_active ? ws_sel_win(&WSS[sel_ws]) : NULL; if (!w || !w->match) { int best = query_best_win(); @@ -6081,16 +6108,19 @@ static void handle_key(const SDL_KeyboardEvent *k) sel_ws = WINS[best].ws; WSS[sel_ws].sel = best - WSS[sel_ws].first; sel_active = true; - w = &WINS[best]; + sel_by_key = true; + } else { + /* no window matched: a workspace whose own name did */ + for (int i = 0; i < NWS; ++i) + if (ci_contains(WSS[i].name, query)) { + act_goto_workspace(&WSS[i]); + return; + } + QUERY_FLASH = now_secs(); /* nothing matched: say so */ + dirty = true; + return; } } - if (w) { act_focus_window(w); return; } - /* no window matched: a workspace whose own name did will do */ - for (int i = 0; i < NWS; ++i) - if (ci_contains(WSS[i].name, query)) { act_goto_workspace(&WSS[i]); return; } - QUERY_FLASH = now_secs(); /* nothing matched: say so */ - dirty = true; - return; } if (ws_sel_win(&WSS[sel_ws])) act_focus_window(ws_sel_win(&WSS[sel_ws])); else act_goto_workspace(&WSS[sel_ws]); @@ -6275,6 +6305,8 @@ static void handle_mouse_press(const SDL_MouseButtonEvent *b) int ws_idx = -1; int win_idx = hit_test(mx, my, &ws_idx); + mouse_moved = true; /* clicking is intent even without moving first */ + if (editing) end_edit(true); /* a click elsewhere commits */ /* a monitor picked up out of the map: it can be put down against @@ -6445,6 +6477,19 @@ static void handle_mouse_motion(const SDL_MouseMotionEvent *mo) drag_x = mx; drag_y = my; + /* A new window gets a motion event for wherever the pointer already is, + * and a hand resting on the mouse sends a stream of them that go nowhere. + * Neither is the user pointing at anything, so the first position is only + * remembered, and the pointer starts selecting once it has left it. */ + if (!mouse_moved) { + if (!mouse_seen) { mouse_seen = true; mouse_x0 = mx; mouse_y0 = my; } + else { + float dx = mx - mouse_x0, dy = my - mouse_y0; + float slop = 3.0f * SC; + if (dx * dx + dy * dy > slop * slop) { mouse_moved = true; dirty = true; } + } + } + if (press_down && !drag_active) { float dx = mx - press_x, dy = my - press_y; if (dx * dx + dy * dy > (7.0f * SC) * (7.0f * SC)) { @@ -6496,20 +6541,31 @@ static void handle_mouse_motion(const SDL_MouseMotionEvent *mo) } if (ph != pencil_hot) { pencil_hot = ph; dirty = true; } - if (ws_idx == hov_ws && win_idx == hov_win) return; - - hov_ws = ws_idx; - hov_win = win_idx; - SDL_SetCursor(ws_idx >= 0 ? CUR_HAND : CUR_ARROW); - dirty = true; + if (ws_idx != hov_ws || win_idx != hov_win) { + hov_ws = ws_idx; + hov_win = win_idx; + SDL_SetCursor(ws_idx >= 0 ? CUR_HAND : CUR_ARROW); + dirty = true; + } /* Pointing at something selects it, so space, x, ctrl+digit and the rest * act on whatever is under the pointer, exactly as they do on whatever - * the arrow keys picked. */ - if (ws_idx >= 0) { - sel_active = true; - sel_ws = ws_idx; - WSS[ws_idx].sel = (win_idx >= 0) ? win_idx - WSS[ws_idx].first : -1; + * the arrow keys picked. + * + * This cannot hang off "what is under the pointer changed": the pointer is + * already somewhere when the overview opens, so the first event it sends + * records that position without selecting anything, and moving inside that + * same tile afterwards changed nothing to notice — the cursor only + * appeared once the pointer crossed into a different workspace. */ + if (ws_idx >= 0 && mouse_moved) { + int want = (win_idx >= 0) ? win_idx - WSS[ws_idx].first : -1; + if (!sel_active || sel_by_key || sel_ws != ws_idx || WSS[ws_idx].sel != want) { + sel_active = true; + sel_by_key = false; /* resting here is not a choice */ + sel_ws = ws_idx; + WSS[ws_idx].sel = want; + dirty = true; + } } } @@ -6642,6 +6698,8 @@ static void backdrop_hover(float fx, float fy) { float x = fx * (float)RW, y = fy * (float)RH; + mouse_moved = true; /* a pointer, just relayed from the window in front */ + int si = slot_at(x, y); if (si >= 0 && SLOTS[si].ws < 0) { /* a free number */ @@ -7414,7 +7472,13 @@ int main(int argc, char **argv) apply_filter(); rebuild_chrome(); + /* Everything above picked a selection for the keyboard to start from, but + * nobody has pressed anything yet, so it is not drawn. start_selection + * still decides what the first arrow key moves away from. */ + sel_by_key = false; + if (SHOT_MX >= 0.0f) { /* pretend the pointer is there */ + mouse_moved = true; /* a shot shows the cursor */ hov_win = hit_test(SHOT_MX * MOUSE_SCALE, SHOT_MY * MOUSE_SCALE, &hov_ws); }