T51: round 1 fix: on X11 the room above runs to the top of the RandR monitor under her, or of _NET_WORKAREA where that lies lower, since the work area spans every monitor; on Wayland she checks twice a second where the window's size and state put her anchor, counted from the pane as gpui counts it, and opens there again when it moved, since gpui hears a change of state alone as no change of bounds and gives an unseen popup no frames; tiling counts where gpui reports it, for a window it draws itself; her tests move to pet/tests.rs, keeping pet.rs under 500 lines
This commit is contained in:
6 files changed
+234
-153
No files matched your search
@@ -109,4 +109,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
||||
- 2026-10-18: herdr's shortcuts, which a user found dead, as kitty and Ghostty send them. herdr 0.9.5 asks for kitty's disambiguate, events and alternates on macOS and Linux, and takes `cmd+shift+ctrl+[` as `[` with Shift, Ctrl and Super, `CSI 91;14u`, the symbol as its alternate, `CSI 91:123;14u`, as Ghostty sends it; gpui named the key `{` and cleared Shift, so ELYTerminal sent `CSI 123;13u`, which herdr matches only to `cmd+ctrl+{`. Under kitty's flags a key Shift made a symbol of now goes as its own character with Shift, the symbol its alternate; legacy keys keep fixterms' symbol without Shift, as Ghostty's do. On macOS the key's own character comes from `UCKeyTranslate` on the event's key code, in the layout gpui reads: Command's while Command is down or while the plain one types no ASCII; AppKit's `charactersByApplyingModifiers:` ignores the layout for events built in-process. On Windows herdr asks for no kitty flags and reads the console's key records, and ConPTY asks every terminal for win32-input-mode, `CSI ? 9001 h`; so, as Windows Terminal does, each key that types no text goes as `CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _`: the virtual key from gpui's name and the layout (`VkKeyScanW`), its scan code, what Windows types for it (`ToUnicode`, a dead key left as it is), down or up, and the modifier keys, locks and enhanced block from `GetKeyState`. kitty's flags go first, as in Windows Terminal; text stays text, so a repeat or a release goes as a record where its press did, a repeat that types after its modifiers came up too, and a release goes so only there; a full reset ends the mode, as Windows Terminal's does. A key that is down is known by its code, AppKit's key code on macOS and the virtual key on Windows, since gpui names a release by the modifiers left down: a release after Shift came up still goes out. Linux has only gpui's name. A console record holds no Windows key, so no Windows terminal passes herdr a `cmd` binding, and a keypad key goes as its main key, since gpui names it so. On Linux gpui keeps neither the key code nor its xkb state, and names a non-ASCII key by its place on a US keyboard, so a key's own character cannot be found: Shift with a symbol and Ctrl, Alt or Super goes as the symbol without Shift, which herdr matches to a binding on the symbol, `ctrl+{`.
|
||||
- 2026-10-18: inside herdr, herdr opens its panes' links, as the owner's Cmd-press and Ctrl-press ask. herdr 0.9.5 wraps a long URL at its pane's edge with no wrap flag and writes its panes' OSC 8 links to the terminal without their targets, so ELYTerminal's own finder opened a cut URL or nothing; herdr itself opens the link under a Ctrl-press, whole from its own pane, a wrapped URL or an OSC 8 target, and underlines it while Ctrl is held. So while herdr's client leads the session and tracks the mouse, every link press goes to herdr as the mouse report it is, and on macOS herdr hears Cmd as Ctrl, the wheel aside. herdr's rules come with it, by its design: while it takes the mouse it keeps its panes' OSC 8 targets from the terminal, so that it alone answers Ctrl-press, and it opens only `http` and `https` links; a `file://` link inside herdr stays shut, since the text the terminal sees may be the label of another link. Shift with the press, which takes the mouse back from a program, is the way past herdr its docs name: the terminal then opens what it sees, a `file://` link too. With herdr's mouse capture off, herdr writes its links' targets out and the terminal opens them itself. Elsewhere ELYTerminal opens the link itself, as before.
|
||||
- 2026-10-18: Ely peeks over the main window's top edge, which the owner asked for after the 0.1.3 film, her head and fingers drawn from the icon's own paths and cut at the edge she holds, in a transparent window of her own, 112 by 107 points with 16 above her for her hops, 140 in from the pane's left edge, less on a pane too narrow for that, so she holds its edge at any width, and the main window seems to hide the rest of her. Her window never takes the keys from the terminal: on macOS her panel becomes key only if needed, which gpui's view never asks; on Windows her window answers a press's activation with none, every other message passed on to gpui's own procedure; on X11 and Wayland a press moves no focus. gpui moves no window and places its popups only on Wayland, so each platform keeps her there its own way: on macOS gpui's popup panel is made the main window's child, at its level and in its Spaces, without a shadow, so AppKit moves it and hides it with the window; on Windows gpui's popup is owned by the main window and loses the topmost place gpui gives it, so it stays just above the main window and hides with it; gpui makes it an overlapped window, which Windows gives a caption and a frame, and opens it as large as the display when the middle of the box asked for lies off every display, as hers does over a window near the screen's top: in 0.1.4 a user found her at the bottom of a framed window the display's size, which took the clicks meant for the windows behind; so it becomes a bare popup, without caption, frame, corners or shadow, and the glue posts her window her bottom left corner on the pane's top edge as the main window moves, since Windows sends a new size at once and gpui hears it only outside its own updates, as gpui defers its own resize; her window sizes her at the DPI it has when the place lands, so her height is her own DPI's and her bottom the pane's top, whatever the two windows' DPI, and a place posted before her DPI changed sizes her at the new one; at a new DPI gpui takes the rect Windows suggests, and once the move that brought it is done she takes her box at that DPI on the corner her last place gave her, while that DPI is still hers and the box lies mostly on a display of it, as Windows reckons a window's display, else Windows' rect stands; so a keep changes no DPI and posts no other, where between two displays a corner kept always, or a keep whose DPI had passed while places piled up, set her DPI back, and back again; on X11 gpui's popup, which no window manager places, is moved onto the pane, stacked just above the window manager's frame as the window moves or comes forward, and unmapped while it hides; on Wayland she is gpui's anchored popup, an xdg_popup on the pane's top edge, which the compositor keeps there; gpui moves a popup's anchor never, so a new width that moves her place opens her again. She opens with the sessions' mood and moves all the time, as the films move her, which the owner asked for after 0.1.4, where she stood still but for a press. She is rigged from the icon's paths as the films rig her, in layers of one color, each an SVG mask gpui rasterizes once at its size and moves with a matrix of its own after its pixels are placed, so she moves between pixels; her head bobs with her breath while her neck, collar and hands stay on the edge, her side locks sway, her ahoge breathes, and she blinks on her own, at times twice. Her mood sets her act: calm, she glances left, then right, her head after her eyes, and dozes after 5 minutes, her eyes falling shut, her head and ahoge drooping, zs floating up; a program that works runs her eyes along the terminal's lines while her fingers tap by turns; one that waits on you widens her eyes, opens her mouth, rocks a mark beside her and hops her twice every 2.6 s; an error raises her brows and shakes her, more softly every 4.5 s after, while a drop runs down; a finish cheers her for 4 s with smiling eyes, a hop and sparkles; a press winks at you while a heart rises. Each act begins with her eyes opening and her mouth popping, and a change of act, a change of mood though the act stays, as inside a wink, and a press each ease her from the pose she was in, in 0.3 s, her brows with her, the marks she leaves fading. She draws at every frame the display gives her window while it shows, and gpui gives a hidden window none; her time is folded into each move's round in f64, so weeks of one mood keep her steps fine. Settings › Terminal › Ely turns her off; she is on by default.
|
||||
- 2026-10-19: Ely stands inside the main window, her bottom on its pane's bottom edge, as far in from the left, while her box would not fit above it, which the owner asked for after 0.1.4: on a window at the screen's top or full screen her titled macOS panel, which AppKit keeps under the menu bar, stuck between the menu bar and the terminal, and on Windows and X11 she stood off the screen. The rule lives in `pet.rs`: she stands on the top edge while the room above the pane, up to the top of the work area of the main window's display, holds her box, 123.4 points; each platform measures the room and places her: macOS from the main window's `frame` and its `screen`'s `visibleFrame`, which leaves out the menu bar, both in AppKit's points; Windows from the client rect and the work area (`rcWork`) of the monitor `MonitorFromWindow` names, at the main window's scale, since above it or inside it she stands on that monitor; X11 from the window's place on the root and the top of `_NET_WORKAREA`, its first desktop's, which window managers keep alike, else the screen's top, as the log says once. A Wayland client knows no place of its own, so there she stands on the bottom edge while the window is full screen, maximized or tiled at its top, which gpui counts as tiled; gpui moves a popup's anchor never, so a change of edge opens her again, as a change of width does. Each place logs the edge she moves to. On WSLg, X11 and Weston's Wayland both put her inside a window as tall as the screen or maximized, and over one with room.
|
||||
- 2026-10-19: Ely stands inside the main window, her bottom on its pane's bottom edge, as far in from the left, while her box would not fit above it, which the owner asked for after 0.1.4: on a window at the screen's top or full screen her titled macOS panel, which AppKit keeps under the menu bar, stuck between the menu bar and the terminal, and on Windows and X11 she stood off the screen. The rule lives in `pet.rs`: she stands on the top edge while the room above the pane, up to the top of the work area of the main window's display, holds her box, 123.4 points; each platform measures the room and places her: macOS from the main window's `frame` and its `screen`'s `visibleFrame`, which leaves out the menu bar, both in AppKit's points; Windows from the client rect and the work area (`rcWork`) of the monitor `MonitorFromWindow` names, at the main window's scale, since above it or inside it she stands on that monitor; X11 from the window's place on the root, up to the top of the RandR monitor under her, or of `_NET_WORKAREA`, its first desktop's, where that lies lower: the work area spans every monitor, so on a lower monitor its top is another's. A Wayland client knows no place of its own, so there she stands on the bottom edge while the window is full screen, maximized or tiled at its top, which gpui reports only for a window it draws itself: under server decorations, as Sway's tiling draws them, she stays on top. gpui moves a popup's anchor never, hears a change of state alone as no change of bounds, and gives an unseen popup no frames, so twice a second she checks where the window's size and state put her anchor, counted from the pane as gpui counts it, and opens there again when it moved. Each place logs the edge she moves to. On WSLg, X11 and Weston's Wayland both put her inside a window as tall as the screen or maximized, and over one with room; headless Sway and a two-monitor Xorg did too.
|
||||
+1
-1
@@ -41,7 +41,7 @@ raw-window-handle = "0.6"
|
||||
wayland-backend = { version = "0.3", features = ["client_system"] }
|
||||
wayland-client = "0.31"
|
||||
wayland-protocols = { version = "0.32", features = ["client", "staging"] }
|
||||
x11rb = "0.13"
|
||||
x11rb = { version = "0.13", features = ["randr"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
wayland-protocols = { version = "0.32", features = ["server", "staging"] }
|
||||
|
||||
@@ -132,7 +132,7 @@ Progress truth. Top to bottom, one at a time. A task is ticked only after its co
|
||||
|
||||
## 28. Ely's place, sleep and frame
|
||||
|
||||
- [ ] T51 Ely stands inside the main window at its bottom left while no room lies above it, which the owner asked for after 0.1.4, where on a window at the screen's top or full screen she stuck between the menu bar and the terminal, on macOS, Windows and Linux: her bottom stands on the pane's top edge while her box fits under the top of the work area of the main window's display, else on the pane's bottom edge, as far in from the left; on Wayland, which tells a client nothing of its place, on the bottom edge while the window is full screen, maximized or tiled at its top.
|
||||
- [ ] T51 Ely stands inside the main window at its bottom left while no room lies above it, which the owner asked for after 0.1.4, where on a window at the screen's top or full screen she stuck between the menu bar and the terminal, on macOS, Windows and Linux: her bottom stands on the pane's top edge while her box fits under the top of the work area of the main window's display, else on the pane's bottom edge, as far in from the left; on Wayland, which tells a client nothing of its place, on the bottom edge while the window is full screen, maximized or tiled at its top as gpui reports it, which it does for a window it draws itself.
|
||||
- [ ] T52 A press wakes Ely from her doze, which the owner asked for: she dozes after five calm minutes since the later of her mood's start and the last press, so a press wakes her for five more.
|
||||
- [ ] T53 Ely's window shows nothing but her, which the owner asked for after a line showed along its top on macOS: there her window is a borderless panel, without the titled frame gpui's popup carries, whose edge macOS draws; on Windows and Linux her window shows no frame, border or shadow.
|
||||
|
||||
|
||||
+74
-141
@@ -4,16 +4,22 @@ mod look;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
mod rig;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod x11;
|
||||
|
||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
App, AppContext as _, Bounds, Context, Decorations, InteractiveElement, IntoElement,
|
||||
MouseButton, ParentElement, Pixels, Render, Styled, Subscription, Window,
|
||||
MouseButton, ParentElement, Pixels, Point, Render, Styled, Subscription, Window,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowHandle, WindowKind, WindowOptions, canvas, div,
|
||||
point, popup, px, size,
|
||||
};
|
||||
@@ -36,6 +42,8 @@ const HEAD: f32 = 107.4;
|
||||
const ROOM: f32 = 16.0;
|
||||
/// How far in from the pane's left edge she holds its top edge.
|
||||
const LEFT: f32 = 140.0;
|
||||
/// How often she checks her Wayland anchor against the main window.
|
||||
const CHECK: Duration = Duration::from_millis(500);
|
||||
|
||||
/// The pane edge her bottom stands on: the top, or the bottom, inside the window.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -44,6 +52,9 @@ enum Edge {
|
||||
Bottom,
|
||||
}
|
||||
|
||||
/// The window she shows in, which the settings open and close.
|
||||
type Shown = Rc<RefCell<Option<WindowHandle<Pet>>>>;
|
||||
|
||||
/// Ely in her window: she moves all the time, her acts follow the sessions' marks, and a press pets her.
|
||||
pub struct Pet {
|
||||
rig: Rc<Rig>,
|
||||
@@ -56,9 +67,9 @@ pub struct Pet {
|
||||
act: Act,
|
||||
last: Option<Pose>,
|
||||
eased: Option<(Pose, Instant)>,
|
||||
/// How far in from the pane's left edge she opened, and the edge she stands on.
|
||||
left: f32,
|
||||
/// The edge she stands on, and on Wayland the anchor her popup opened at, from the pane's top left, which never moves.
|
||||
edge: Edge,
|
||||
anchor: Option<Point<Pixels>>,
|
||||
/// What keeps her window on the main window's edge, where the platform does not.
|
||||
glue: Option<platform::Glue>,
|
||||
_watch: Vec<Subscription>,
|
||||
@@ -66,7 +77,7 @@ pub struct Pet {
|
||||
|
||||
/// Shows Ely over `main` while the settings want her.
|
||||
pub fn follow(main: WindowHandle<Workspace>, cx: &mut App) {
|
||||
let shown: Rc<RefCell<Option<WindowHandle<Pet>>>> = Rc::default();
|
||||
let shown: Shown = Rc::default();
|
||||
let sync = {
|
||||
let shown = shown.clone();
|
||||
move |cx: &mut App| {
|
||||
@@ -83,29 +94,17 @@ pub fn follow(main: WindowHandle<Workspace>, cx: &mut App) {
|
||||
};
|
||||
sync(cx);
|
||||
cx.observe_global::<SettingsStore>(sync).detach();
|
||||
// A Wayland popup keeps the place it opened at, so she opens again where a new width puts her.
|
||||
let moved = main.update(cx, |_, window, cx| {
|
||||
cx.observe_window_bounds(window, move |_, window, cx| {
|
||||
let Some(pet) = *shown.borrow() else {
|
||||
return;
|
||||
};
|
||||
let at = (left(pane(window).size.width), wayland_edge(window));
|
||||
if wayland(window) && pet.read(cx).is_ok_and(|pet| (pet.left, pet.edge) != at) {
|
||||
let shown = shown.clone();
|
||||
cx.defer(move |cx| {
|
||||
close(
|
||||
pet,
|
||||
"to stand where the window's new size or state puts her",
|
||||
cx,
|
||||
);
|
||||
*shown.borrow_mut() = open_pet(main, cx);
|
||||
});
|
||||
// gpui hears a change of state alone as no change of bounds, and sends an unseen popup no frames.
|
||||
if main
|
||||
.update(cx, |_, window, _| wayland(window))
|
||||
.is_ok_and(|wayland| wayland)
|
||||
{
|
||||
cx.spawn(async move |cx| {
|
||||
while cx.update(|cx| reanchor(main, &shown, cx)).is_ok() {
|
||||
cx.background_executor().timer(CHECK).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
match moved {
|
||||
Ok(moved) => moved.detach(),
|
||||
Err(error) => log::error!("pet: the main window's width goes unwatched: {error:#}"),
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,35 +149,67 @@ fn wayland_edge(window: &Window) -> Edge {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where she stands over a Wayland window: the edge, and her anchor counted from the pane's top left, as gpui's popups count.
|
||||
fn wayland_spot(window: &Window) -> (Edge, Point<Pixels>) {
|
||||
let pane = pane(window);
|
||||
let edge = wayland_edge(window);
|
||||
let y = match edge {
|
||||
Edge::Top => px(0.),
|
||||
Edge::Bottom => pane.size.height - px(1.),
|
||||
};
|
||||
(edge, point(px(left(pane.size.width)), y))
|
||||
}
|
||||
|
||||
/// Opens her again over a Wayland `main` once its size or state moves the anchor her popup opened at, which never moves.
|
||||
fn reanchor(main: WindowHandle<Workspace>, shown: &Shown, cx: &mut App) -> anyhow::Result<()> {
|
||||
let spot = main.update(cx, |_, window, _| wayland_spot(window))?;
|
||||
let Some(pet) = *shown.borrow() else {
|
||||
return Ok(());
|
||||
};
|
||||
if pet
|
||||
.read(cx)
|
||||
.is_ok_and(|pet| (pet.edge, pet.anchor) == (spot.0, Some(spot.1)))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
close(
|
||||
pet,
|
||||
"to stand where the window's new size or state puts her",
|
||||
cx,
|
||||
);
|
||||
*shown.borrow_mut() = open_pet(main, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Opens her window on `main`'s pane: a popup Wayland's compositor places, else a window kept there.
|
||||
fn show(main: WindowHandle<Workspace>, cx: &mut App) -> anyhow::Result<WindowHandle<Pet>> {
|
||||
let (bounds, pane, wayland, edge) = main.update(cx, |_, window, _| {
|
||||
let wayland = wayland(window);
|
||||
let edge = if wayland {
|
||||
wayland_edge(window)
|
||||
} else {
|
||||
Edge::Top
|
||||
};
|
||||
(window.bounds(), pane(window), wayland, edge)
|
||||
let (bounds, pane, wayland, (edge, spot)) = main.update(cx, |_, window, _| {
|
||||
(
|
||||
window.bounds(),
|
||||
pane(window),
|
||||
wayland(window),
|
||||
wayland_spot(window),
|
||||
)
|
||||
})?;
|
||||
let at = left(pane.size.width);
|
||||
let room = size(px(WIDTH), px(HEAD + ROOM));
|
||||
let kind = if wayland {
|
||||
let (y, anchor) = match edge {
|
||||
Edge::Top => (pane.origin.y, popup::PopupAnchor::TopLeft),
|
||||
Edge::Bottom => (pane.bottom() - px(1.), popup::PopupAnchor::BottomLeft),
|
||||
let (edge, kind) = if wayland {
|
||||
let anchor = match edge {
|
||||
Edge::Top => popup::PopupAnchor::TopLeft,
|
||||
Edge::Bottom => popup::PopupAnchor::BottomLeft,
|
||||
};
|
||||
WindowKind::AnchoredPopup(popup::PopupOptions {
|
||||
let kind = WindowKind::AnchoredPopup(popup::PopupOptions {
|
||||
parent: main.into(),
|
||||
anchor_rect: Bounds::new(point(pane.origin.x + px(at), y), size(px(1.), px(1.))),
|
||||
anchor_rect: Bounds::new(pane.origin + spot, size(px(1.), px(1.))),
|
||||
anchor,
|
||||
gravity: popup::PopupGravity::TopRight,
|
||||
constraint_adjustment: popup::PopupConstraintAdjustment::empty(),
|
||||
offset: point(px(0.), px(0.)),
|
||||
grab: false,
|
||||
})
|
||||
});
|
||||
(edge, kind)
|
||||
} else {
|
||||
WindowKind::PopUp
|
||||
(Edge::Top, WindowKind::PopUp)
|
||||
};
|
||||
let origin = bounds.origin + pane.origin + point(px(at), -room.height);
|
||||
let options = WindowOptions {
|
||||
@@ -221,8 +252,8 @@ fn show(main: WindowHandle<Workspace>, cx: &mut App) -> anyhow::Result<WindowHan
|
||||
act: Act::Calm,
|
||||
last: None,
|
||||
eased: None,
|
||||
left: at,
|
||||
edge,
|
||||
anchor: wayland.then_some(spot),
|
||||
glue: None,
|
||||
_watch: vec![feel],
|
||||
}
|
||||
@@ -385,101 +416,3 @@ impl Render for Pet {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{look::Mark, *};
|
||||
|
||||
/// Her, `mood` since `since`, come long before.
|
||||
fn ely(mood: Mood, since: Instant) -> Pet {
|
||||
Pet {
|
||||
rig: Rc::new(Rig::new()),
|
||||
mood,
|
||||
since,
|
||||
shown: since - Duration::from_secs(60),
|
||||
petted: None,
|
||||
act: Act::Calm,
|
||||
last: None,
|
||||
eased: None,
|
||||
left: LEFT,
|
||||
edge: Edge::Top,
|
||||
glue: None,
|
||||
_watch: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_change_of_mood_or_a_press_eases_her_from_where_she_is_though_the_act_stays() {
|
||||
let start = Instant::now();
|
||||
let at = |ms: u64| start + Duration::from_millis(ms);
|
||||
// A wait hops her, a press winks, and a program starts working inside the wink.
|
||||
let mut pet = ely(Mood::Waiting, start);
|
||||
pet.press(at(2250));
|
||||
let before = pet.pose(at(2750));
|
||||
pet.feel(Mood::Working, at(2750));
|
||||
let after = pet.pose(at(2751));
|
||||
assert_eq!((before.act, after.act), (Act::Wink, Act::Wink));
|
||||
assert!(
|
||||
(after.lift - before.lift).abs() < 1.0,
|
||||
"no drop: {} then {}",
|
||||
before.lift,
|
||||
after.lift
|
||||
);
|
||||
let marks: Vec<Mark> = after.marks.iter().map(|(mark, _)| *mark).collect();
|
||||
assert!(
|
||||
marks.contains(&Mark::Bang),
|
||||
"the wait's mark fades: {marks:?}"
|
||||
);
|
||||
// An old finish clears mid-glance: calm before and after, her eyes turn back at her pace.
|
||||
let mut pet = ely(Mood::Done, start);
|
||||
let before = pet.pose(at(13_600));
|
||||
pet.feel(Mood::Calm, at(13_600));
|
||||
let after = pet.pose(at(13_601));
|
||||
assert_eq!((before.act, after.act), (Act::Calm, Act::Calm));
|
||||
assert!(
|
||||
before.look.0 < -5.0 && (after.look.0 - before.look.0).abs() < 0.5,
|
||||
"no snap: {:?} then {:?}",
|
||||
before.look,
|
||||
after.look
|
||||
);
|
||||
let later = pet.pose(at(14_000));
|
||||
assert!(
|
||||
pet.eased.is_none() && later.look.0.abs() < 0.5,
|
||||
"eased in by then"
|
||||
);
|
||||
// A second press mid-hop restarts the wink without a drop.
|
||||
let mut pet = ely(Mood::Calm, start);
|
||||
pet.press(at(1000));
|
||||
let before = pet.pose(at(1300));
|
||||
pet.press(at(1300));
|
||||
let after = pet.pose(at(1301));
|
||||
assert!(
|
||||
before.lift > 10.0 && (after.lift - before.lift).abs() < 1.0,
|
||||
"no drop: {} then {}",
|
||||
before.lift,
|
||||
after.lift
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn she_stands_on_the_top_edge_while_her_box_fits_above_it() {
|
||||
assert_eq!(edge(HEAD + ROOM), Edge::Top, "just room for her");
|
||||
assert_eq!(edge(400.0), Edge::Top);
|
||||
assert_eq!(edge(HEAD + ROOM - 0.5), Edge::Bottom, "half a point short");
|
||||
assert_eq!(edge(0.0), Edge::Bottom, "a window at the work area's top");
|
||||
assert_eq!(edge(-30.0), Edge::Bottom, "a window above it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn she_holds_the_edge_of_any_pane() {
|
||||
assert_eq!(left(px(1200.)), LEFT);
|
||||
assert_eq!(left(px(200.)), 200.0 - WIDTH);
|
||||
assert_eq!(
|
||||
left(px(100.)),
|
||||
0.0,
|
||||
"narrower than she is, she stands at its left"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{look::Mark, *};
|
||||
|
||||
/// Her, `mood` since `since`, come long before.
|
||||
fn ely(mood: Mood, since: Instant) -> Pet {
|
||||
Pet {
|
||||
rig: Rc::new(Rig::new()),
|
||||
mood,
|
||||
since,
|
||||
shown: since - Duration::from_secs(60),
|
||||
petted: None,
|
||||
act: Act::Calm,
|
||||
last: None,
|
||||
eased: None,
|
||||
edge: Edge::Top,
|
||||
anchor: None,
|
||||
glue: None,
|
||||
_watch: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_change_of_mood_or_a_press_eases_her_from_where_she_is_though_the_act_stays() {
|
||||
let start = Instant::now();
|
||||
let at = |ms: u64| start + Duration::from_millis(ms);
|
||||
// A wait hops her, a press winks, and a program starts working inside the wink.
|
||||
let mut pet = ely(Mood::Waiting, start);
|
||||
pet.press(at(2250));
|
||||
let before = pet.pose(at(2750));
|
||||
pet.feel(Mood::Working, at(2750));
|
||||
let after = pet.pose(at(2751));
|
||||
assert_eq!((before.act, after.act), (Act::Wink, Act::Wink));
|
||||
assert!(
|
||||
(after.lift - before.lift).abs() < 1.0,
|
||||
"no drop: {} then {}",
|
||||
before.lift,
|
||||
after.lift
|
||||
);
|
||||
let marks: Vec<Mark> = after.marks.iter().map(|(mark, _)| *mark).collect();
|
||||
assert!(
|
||||
marks.contains(&Mark::Bang),
|
||||
"the wait's mark fades: {marks:?}"
|
||||
);
|
||||
// An old finish clears mid-glance: calm before and after, her eyes turn back at her pace.
|
||||
let mut pet = ely(Mood::Done, start);
|
||||
let before = pet.pose(at(13_600));
|
||||
pet.feel(Mood::Calm, at(13_600));
|
||||
let after = pet.pose(at(13_601));
|
||||
assert_eq!((before.act, after.act), (Act::Calm, Act::Calm));
|
||||
assert!(
|
||||
before.look.0 < -5.0 && (after.look.0 - before.look.0).abs() < 0.5,
|
||||
"no snap: {:?} then {:?}",
|
||||
before.look,
|
||||
after.look
|
||||
);
|
||||
let later = pet.pose(at(14_000));
|
||||
assert!(
|
||||
pet.eased.is_none() && later.look.0.abs() < 0.5,
|
||||
"eased in by then"
|
||||
);
|
||||
// A second press mid-hop restarts the wink without a drop.
|
||||
let mut pet = ely(Mood::Calm, start);
|
||||
pet.press(at(1000));
|
||||
let before = pet.pose(at(1300));
|
||||
pet.press(at(1300));
|
||||
let after = pet.pose(at(1301));
|
||||
assert!(
|
||||
before.lift > 10.0 && (after.lift - before.lift).abs() < 1.0,
|
||||
"no drop: {} then {}",
|
||||
before.lift,
|
||||
after.lift
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn she_stands_on_the_top_edge_while_her_box_fits_above_it() {
|
||||
assert_eq!(edge(HEAD + ROOM), Edge::Top, "just room for her");
|
||||
assert_eq!(edge(400.0), Edge::Top);
|
||||
assert_eq!(edge(HEAD + ROOM - 0.5), Edge::Bottom, "half a point short");
|
||||
assert_eq!(edge(0.0), Edge::Bottom, "a window at the work area's top");
|
||||
assert_eq!(edge(-30.0), Edge::Bottom, "a window above it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn she_holds_the_edge_of_any_pane() {
|
||||
assert_eq!(left(px(1200.)), LEFT);
|
||||
assert_eq!(left(px(200.)), 200.0 - WIDTH);
|
||||
assert_eq!(
|
||||
left(px(100.)),
|
||||
0.0,
|
||||
"narrower than she is, she stands at its left"
|
||||
);
|
||||
}
|
||||
+63
-9
@@ -4,7 +4,10 @@ use anyhow::{Context as _, bail};
|
||||
use gpui::{Bounds, Pixels, Window};
|
||||
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
use x11rb::{
|
||||
protocol::xproto::{Atom, AtomEnum, ConfigureWindowAux, ConnectionExt as _, StackMode},
|
||||
protocol::{
|
||||
randr::ConnectionExt as _,
|
||||
xproto::{Atom, AtomEnum, ConfigureWindowAux, ConnectionExt as _, StackMode},
|
||||
},
|
||||
rust_connection::RustConnection,
|
||||
};
|
||||
|
||||
@@ -33,16 +36,20 @@ impl Glue {
|
||||
main,
|
||||
workarea,
|
||||
};
|
||||
match glue.work_top(glue.frame()?.0)? {
|
||||
Some(top) => log::info!("pet: the work area starts {top} pixels down the screen"),
|
||||
let (root, _) = glue.frame()?;
|
||||
let monitors = glue.monitors(root)?.len();
|
||||
match glue.work_top(root)? {
|
||||
Some(top) => {
|
||||
log::info!("pet: {monitors} monitors; the work area starts {top} pixels down")
|
||||
}
|
||||
None => log::info!(
|
||||
"pet: the window manager names no work area; the screen's top bounds her"
|
||||
"pet: {monitors} monitors; the window manager names no work area, so each monitor's top bounds her"
|
||||
),
|
||||
}
|
||||
Ok(glue)
|
||||
}
|
||||
|
||||
/// Stands her on the main window's pane, on its top edge while her box fits under the top of the work area, else on its bottom edge, as far in as its width allows, stacked just above the main window's frame, and gives the edge.
|
||||
/// Stands her on the main window's pane, on its top edge while her box fits under the top of the monitor under her, or of the work area where that lies lower, else on its bottom edge, as far in as its width allows, stacked just above the main window's frame, and gives the edge.
|
||||
pub fn place(&self, main: &Window) -> anyhow::Result<Edge> {
|
||||
self.put(main.scale_factor(), pane(main))
|
||||
}
|
||||
@@ -55,15 +62,17 @@ impl Glue {
|
||||
.reply()
|
||||
.context("pet: the main window's place")?;
|
||||
let down = |y: Pixels| i32::from(at.dst_y) + (f32::from(y) * scale).round() as i32;
|
||||
let room = down(pane.origin.y) - self.work_top(root)?.unwrap_or(0);
|
||||
let edge = edge(room as f32 / scale);
|
||||
let x = f32::from(pane.origin.x) + left(pane.size.width);
|
||||
let x = i32::from(at.dst_x) + (x * scale).round() as i32;
|
||||
let top = down(pane.origin.y);
|
||||
let usable = usable(&self.monitors(root)?, self.work_top(root)?, (x, top));
|
||||
let edge = edge(usable.map_or(0.0, |usable| (top - usable) as f32 / scale));
|
||||
let bottom = down(match edge {
|
||||
Edge::Top => pane.origin.y,
|
||||
Edge::Bottom => pane.bottom(),
|
||||
});
|
||||
let x = f32::from(pane.origin.x) + left(pane.size.width);
|
||||
let aux = ConfigureWindowAux::new()
|
||||
.x(i32::from(at.dst_x) + (x * scale).round() as i32)
|
||||
.x(x)
|
||||
.y(bottom - ((HEAD + ROOM) * scale).round() as i32)
|
||||
.sibling(frame)
|
||||
.stack_mode(StackMode::ABOVE);
|
||||
@@ -74,6 +83,18 @@ impl Glue {
|
||||
Ok(edge)
|
||||
}
|
||||
|
||||
/// Each monitor's x, y, width and height on the root, as RandR names them.
|
||||
fn monitors(&self, root: u32) -> anyhow::Result<Vec<[i32; 4]>> {
|
||||
let reply = (self.connection)
|
||||
.randr_get_monitors(root, true)?
|
||||
.reply()
|
||||
.context("pet: the monitors")?;
|
||||
let monitors = reply.monitors.iter();
|
||||
Ok(monitors
|
||||
.map(|m| [m.x.into(), m.y.into(), m.width.into(), m.height.into()])
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// The top of the work area the window manager names in `_NET_WORKAREA`, its first desktop's, which managers keep alike; none where it names none.
|
||||
fn work_top(&self, root: u32) -> anyhow::Result<Option<i32>> {
|
||||
let reply = (self.connection)
|
||||
@@ -111,6 +132,14 @@ impl Glue {
|
||||
}
|
||||
}
|
||||
|
||||
/// The top of the room over `(x, y)`: the top of the monitor it lies on, or the work area's top `area` where that lies lower; none off every monitor.
|
||||
fn usable(monitors: &[[i32; 4]], area: Option<i32>, (x, y): (i32, i32)) -> Option<i32> {
|
||||
let [_, top, _, _] = monitors.iter().find(|[left, top, width, height]| {
|
||||
(*left..left + width).contains(&x) && (*top..top + height).contains(&y)
|
||||
})?;
|
||||
Some(area.map_or(*top, |area| area.max(*top)))
|
||||
}
|
||||
|
||||
/// The X window gpui made for `window`.
|
||||
fn native(window: &Window) -> anyhow::Result<u32> {
|
||||
let handle = HasWindowHandle::window_handle(window)
|
||||
@@ -135,6 +164,31 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::testx11::Xvfb;
|
||||
|
||||
#[test]
|
||||
fn the_room_above_runs_to_the_top_of_the_monitor_under_her_or_of_the_work_area() {
|
||||
let stacked = [[0, 0, 1280, 800], [0, 800, 1280, 800]];
|
||||
assert_eq!(
|
||||
usable(&stacked, None, (240, 800)),
|
||||
Some(800),
|
||||
"the lower monitor's top"
|
||||
);
|
||||
assert_eq!(
|
||||
usable(&stacked, Some(0), (240, 800)),
|
||||
Some(800),
|
||||
"a work area over both monitors leaves the lower one's top"
|
||||
);
|
||||
assert_eq!(
|
||||
usable(&stacked, Some(32), (240, 300)),
|
||||
Some(32),
|
||||
"a panel atop the upper one"
|
||||
);
|
||||
assert_eq!(
|
||||
usable(&stacked, None, (2000, 300)),
|
||||
None,
|
||||
"off every monitor"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn she_stands_on_an_edge_of_the_pane_just_above_the_frame_and_hides_with_it() {
|
||||
let xvfb = Xvfb::start();
|
||||
|
||||
Reference in new issue
Block a user