Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions crates/tinyclient/src/gui/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,17 @@ impl Render for Pane {
.size_full();
let after = gpui::canvas(
|_, _, _| (),
move |_, (), window, cx| this.update(cx, |this, cx| this.after_layout(window, cx)),
move |_, (), window, cx| {
this.update(cx, |this, cx| this.after_layout(window, cx));
let pane = this.clone();
// Track geometry before hit testing/occlusion can hide the pane behind
// its own toolbar. Do not consume the event: buttons still receive it.
window.on_mouse_event(move |event: &gpui::MouseMoveEvent, phase, _, cx| {
if phase == gpui::DispatchPhase::Capture {
pane.update(cx, |this, cx| this.point_at(event.position, cx));
}
});
},
)
.absolute()
.size_0();
Expand Down Expand Up @@ -1173,10 +1183,10 @@ impl Render for Pane {
}
}),
)
.on_mouse_move(cx.listener(|this, event: &gpui::MouseMoveEvent, _, cx| this.point_at(event.position, cx)))
.on_hover(cx.listener(|this, hovered: &bool, _, cx| {
.on_hover(cx.listener(|this, hovered: &bool, window, cx| {
if !*hovered {
this.unpoint(cx);
// Occlusion is not a pointer exit; recheck the row/toolbar bounds.
this.point_at(window.mouse_position(), cx);
}
}))
.child(rows)
Expand Down
63 changes: 40 additions & 23 deletions crates/tinyclient/src/gui/conversation/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ use std::rc::Rc;
const TOOLBAR_RISE: f32 = 14.;
const TOOLBAR_REACH: f32 = 320.;

/// Hover ownership is geometric: the toolbar occludes the pane's hitbox, but
/// crossing onto its buttons must keep the current hover owner.
fn pointed_row(position: Point<Pixels>, viewport: Bounds<Pixels>, seen: &[(usize, Bounds<Pixels>)], held: Option<usize>) -> Option<usize> {
if !viewport.contains(&position) { return None; }
let toolbar = held.and_then(|held| seen.iter().find(|(ix, bounds)| {
let rise = Bounds::from_corners(
point(bounds.right() - px(TOOLBAR_REACH), bounds.top() - px(TOOLBAR_RISE)),
point(bounds.right(), bounds.top() + px(TOOLBAR_RISE + 8.)),
);
*ix == held && rise.contains(&position)
}));
toolbar.or_else(|| seen.iter().find(|(_, bounds)| bounds.contains(&position))).map(|(ix, _)| *ix)
}

/// The context menu of a message, where the pointer was.
pub struct Menu {
pub message: Rc<Message>,
Expand Down Expand Up @@ -197,36 +211,19 @@ impl Pane {
if self.script.holds_hover() {
return;
}
let viewport = self.list.viewport_bounds();
let over = viewport.contains(&position).then(|| {
let over = {
let seen = self.seen.borrow();
let row = |bounds: &Bounds<Pixels>| bounds.contains(&position);
// The toolbar rises over the row above; the pointer on it is still on its own row.
let held = self.hover.as_ref().and_then(|id| {
seen.iter().find(|(ix, bounds)| {
let rise = Bounds::from_corners(
point(bounds.right() - px(TOOLBAR_REACH), bounds.top() - px(TOOLBAR_RISE)),
point(bounds.right(), bounds.top() + px(TOOLBAR_RISE + 8.)),
);
rise.contains(&position) && self.items.get(*ix).and_then(Item::message).is_some_and(|m| m.id == *id)
})
});
let found = held.or_else(|| seen.iter().find(|(_, bounds)| row(bounds)));
found.and_then(|(ix, _)| self.items.get(*ix)).and_then(Item::message).filter(|m| self.can(m).act).map(|m| m.id.clone())
});
let over = over.flatten();
let held = self.hover.as_ref().and_then(|id| seen.iter().find(|(ix, _)| self.items.get(*ix).and_then(Item::message).is_some_and(|m| m.id == *id))).map(|(ix, _)| *ix);
pointed_row(position, self.list.viewport_bounds(), &seen, held)
.and_then(|ix| self.items.get(ix)).and_then(Item::message)
.filter(|m| self.can(m).act).map(|m| m.id.clone())
};
if over != self.hover {
self.hover = over;
cx.notify();
}
}

pub(super) fn unpoint(&mut self, cx: &mut Context<Self>) {
if self.menu.is_none() && self.picker.is_none() && self.hover.take().is_some() {
cx.notify();
}
}

// The toolbar.

/// A menu, picker, dialog, viewer or a window-level layer is up: no toolbar shows.
Expand Down Expand Up @@ -731,6 +728,26 @@ impl Pane {
mod tests {
use super::*;

#[test]
fn crossing_onto_toolbar_buttons_keeps_the_same_message() {
let viewport = Bounds::from_corners(point(px(0.), px(0.)), point(px(800.), px(600.)));
let seen = [
(0, Bounds::from_corners(point(px(0.), px(100.)), point(px(800.), px(150.)))),
(1, Bounds::from_corners(point(px(0.), px(150.)), point(px(800.), px(200.)))),
];
let mut held = pointed_row(point(px(400.), px(175.)), viewport, &seen, None);
assert_eq!(held, Some(1));
for position in [point(px(700.), px(151.)), point(px(700.), px(140.)), point(px(760.), px(140.))] {
// The latter points are above the message, in the occluding toolbar.
held = pointed_row(position, viewport, &seen, held);
assert_eq!(held, Some(1));
assert_eq!(pointed_row(position, viewport, &seen, held), held, "stationary redraw/press keeps the button's owner");
}
assert_eq!(pointed_row(point(px(100.), px(140.)), viewport, &seen, held), Some(0));
assert_eq!(pointed_row(point(px(801.), px(140.)), viewport, &seen, held), None);
assert_eq!(pointed_row(point(px(700.), px(-1.)), viewport, &seen, held), None);
}

#[test]
fn a_message_link_is_built_from_ids_where_the_network_has_one() {
let mut key = protocol::Key { network: Network::Discord, account: "1".into(), id: "C9".into() };
Expand Down
5 changes: 5 additions & 0 deletions docs/design/polish/01-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ toolbar hides. Destructive items sit last after a separator, `danger` text, `dan
the only item (`menu-net` gains "Open in Discord" and "Account settings…" above "Sign out"). **Popover**: S§3, 200–360
wide (pickers declare their own size), 6 from the anchor, no arrow.

**Message action bar.** Hover ownership follows pointer geometry across the
message and its raised action bar. The bar's occlusion of the pane does not
count as leaving it. Moving onto a button or pressing it keeps the same message;
leaving the pane clears hover. Pointer tracking does not consume button events.

**Buttons.** S§3: primary, secondary, ghost, danger, icon. Accept = small primary; Decline = small secondary; no green
buttons, no full-radius controls (Leave, hang up, jump-to-bottom and viewer discs become radius 6 / 8 squares).
In-caption actions ("Retry") are small ghost buttons.
Expand Down
Loading