| ... | @@ -25,6 +25,8 @@ use ui::{Flags, Id, Spec, children, fill, fit, px}; | ... | @@ -25,6 +25,8 @@ use ui::{Flags, Id, Spec, children, fill, fit, px}; |
| 25 | /// The side of the picture sent, in pixels. | 25 | /// The side of the picture sent, in pixels. |
| 26 | const PICTURE: u32 = 96; | 26 | const PICTURE: u32 = 96; |
| 27 | const AVATAR: f32 = 22.0; | 27 | const AVATAR: f32 = 22.0; |
| | 28 | /// The avatars shown before the rest bundle into a chip. |
| | 29 | const STACKED: usize = 3; |
| 28 | /// The name flag above a caret, and how long it shows after the caret last moved. | 30 | /// The name flag above a caret, and how long it shows after the caret last moved. |
| 29 | const FLAG: f32 = 15.0; | 31 | const FLAG: f32 = 15.0; |
| 30 | const FLAG_PAD: f32 = 4.0; | 32 | const FLAG_PAD: f32 = 4.0; |
| ... | @@ -259,6 +261,13 @@ pub(crate) fn joined(cache: &Path, welcome: live::wire::Welcome) -> io::Result<S | ... | @@ -259,6 +261,13 @@ pub(crate) fn joined(cache: &Path, welcome: live::wire::Welcome) -> io::Result<S |
| 259 | Ok(location) | 261 | Ok(location) |
| 260 | } | 262 | } |
| 261 | | 263 | |
| | 264 | /// A peer to go to, since when, and the section and page already asked to open. |
| | 265 | type Going = ( |
| | 266 | [u8; 16], |
| | 267 | std::time::Instant, |
| | 268 | Option<(Option<[u8; 16]>, Option<live::Guid>)>, |
| | 269 | ); |
| | 270 | |
| 262 | /// What a thread of its own sends the frame, and where the frame takes it. | 271 | /// What a thread of its own sends the frame, and where the frame takes it. |
| 263 | pub(crate) type Channel<T> = (mpsc::Sender<T>, mpsc::Receiver<T>); | 272 | pub(crate) type Channel<T> = (mpsc::Sender<T>, mpsc::Receiver<T>); |
| 264 | | 273 | |
| ... | @@ -286,6 +295,10 @@ pub(crate) struct Peers { | ... | @@ -286,6 +295,10 @@ pub(crate) struct Peers { |
| 286 | pictures: HashMap<[u8; 16], Option<draw::RasterImage>>, | 295 | pictures: HashMap<[u8; 16], Option<draw::RasterImage>>, |
| 287 | /// Where each peer's caret was last seen, and when it got there. | 296 | /// Where each peer's caret was last seen, and when it got there. |
| 288 | moved: HashMap<[u8; 16], (Caret, std::time::Instant)>, | 297 | moved: HashMap<[u8; 16], (Caret, std::time::Instant)>, |
| | 298 | /// Where each peer was last seen, and when they last moved, for ordering the avatars. |
| | 299 | active: HashMap<[u8; 16], (Option<live::Presence>, std::time::Instant)>, |
| | 300 | /// The peer an avatar's click goes to, since when, and the place already asked to open. |
| | 301 | going: Option<Going>, |
| 289 | pub(crate) share: Option<crate::share::ShareDialog>, | 302 | pub(crate) share: Option<crate::share::ShareDialog>, |
| 290 | pub(crate) join: Option<crate::share::JoinDialog>, | 303 | pub(crate) join: Option<crate::share::JoinDialog>, |
| 291 | } | 304 | } |
| ... | @@ -295,6 +308,7 @@ impl State { | ... | @@ -295,6 +308,7 @@ impl State { |
| 295 | /// window is: in the shown notebook's room, and in each Live Share's. | 308 | /// window is: in the shown notebook's room, and in each Live Share's. |
| 296 | pub(crate) fn follow_peers(&mut self) { | 309 | pub(crate) fn follow_peers(&mut self) { |
| 297 | self.follow_shares(); | 310 | self.follow_shares(); |
| | 311 | self.go_to_peer(); |
| 298 | let shown = self | 312 | let shown = self |
| 299 | .session | 313 | .session |
| 300 | .as_ref() | 314 | .as_ref() |
| ... | @@ -587,23 +601,86 @@ impl State { | ... | @@ -587,23 +601,86 @@ impl State { |
| 587 | pages | 601 | pages |
| 588 | } | 602 | } |
| 589 | | 603 | |
| 590 | /// The others with the notebook open, as avatars leftward from the search box: a click | 604 | /// The others with the notebook open, as avatars leftward from the search box, the most |
| 591 | /// opens the page someone has open in this section. | 605 | /// lately active nearest it: past `STACKED` the rest are a chip that lists everyone. A |
| | 606 | /// click goes to where someone is. |
| 592 | pub(crate) fn avatars(&mut self) { | 607 | pub(crate) fn avatars(&mut self) { |
| 593 | let peers = self.connected(); | 608 | let mut peers = self.connected(); |
| 594 | if peers.is_empty() { | 609 | if peers.is_empty() { |
| 595 | return; | 610 | return; |
| 596 | } | 611 | } |
| | 612 | let now = std::time::Instant::now(); |
| | 613 | for peer in &peers { |
| | 614 | let id = peer.hello.peer; |
| | 615 | let seen = self |
| | 616 | .peers |
| | 617 | .active |
| | 618 | .entry(id) |
| | 619 | .or_insert((peer.presence.clone(), now)); |
| | 620 | if seen.0 != peer.presence { |
| | 621 | *seen = (peer.presence.clone(), now); |
| | 622 | } |
| | 623 | } |
| | 624 | let active = |peer: &Peer| self.peers.active.get(&peer.hello.peer).map(|(_, at)| *at); |
| | 625 | peers.sort_by_key(|peer| std::cmp::Reverse(active(peer))); |
| | 626 | let shown = if peers.len() > STACKED + 1 { |
| | 627 | STACKED |
| | 628 | } else { |
| | 629 | peers.len() |
| | 630 | }; |
| 597 | self.ui.open( | 631 | self.ui.open( |
| 598 | "peers", | 632 | "peers", |
| 599 | Spec { | 633 | Spec { |
| 600 | size: [children(), px(TAB_ROW)], | 634 | size: [children(), px(TAB_ROW)], |
| 601 | pad: [6.0, (TAB_ROW - AVATAR) / 2.0], | 635 | pad: [6.0, (TAB_ROW - AVATAR) / 2.0], |
| 602 | gap: 4.0, | 636 | gap: -6.0, |
| 603 | ..Spec::default() | 637 | ..Spec::default() |
| 604 | }, | 638 | }, |
| 605 | ); | 639 | ); |
| 606 | for peer in peers.iter().rev() { | 640 | let mut chosen = None; |
| | 641 | if peers.len() > shown { |
| | 642 | let more = format!("+{}", peers.len() - shown); |
| | 643 | let chip = self.ui.leaf( |
| | 644 | "more", |
| | 645 | Spec { |
| | 646 | flags: Flags::CLICKABLE, |
| | 647 | size: [fit(), px(AVATAR)], |
| | 648 | text: Some(&more), |
| | 649 | font_size: Some(10.0), |
| | 650 | bold: true, |
| | 651 | center: true, |
| | 652 | fill: Some(self.ui.theme.chip), |
| | 653 | radius: AVATAR / 2.0, |
| | 654 | pad: [6.0, 0.0], |
| | 655 | role: Some(accesskit::Role::Button), |
| | 656 | ..Spec::default() |
| | 657 | }, |
| | 658 | ); |
| | 659 | let label = format!("{} people here", peers.len()); |
| | 660 | ui::popup::tooltip(&mut self.ui, &label, "", None); |
| | 661 | let menu = Id::ROOT.child("peers-menu"); |
| | 662 | if chip.clicked { |
| | 663 | self.ui.open_popup(menu); |
| | 664 | } |
| | 665 | let places: Vec<String> = (peers.iter()) |
| | 666 | .map(|peer| self.place_of(peer).unwrap_or_default()) |
| | 667 | .collect(); |
| | 668 | let items: Vec<ui::popup::Item> = (peers.iter().zip(&places)) |
| | 669 | .map(|(peer, place)| ui::popup::Item { |
| | 670 | text: &peer.hello.name, |
| | 671 | shortcut: place, |
| | 672 | ..ui::popup::Item::default() |
| | 673 | }) |
| | 674 | .collect(); |
| | 675 | let rect = self.ui.rect(self.ui.id("more")); |
| | 676 | if let Some(rect) = rect |
| | 677 | && let Some(index) = |
| | 678 | ui::popup::menu(&mut self.ui, menu, ui::Anchor::Below(rect), &items, None) |
| | 679 | { |
| | 680 | chosen = Some(peers[index].hello.peer); |
| | 681 | } |
| | 682 | } |
| | 683 | for peer in peers[..shown].iter().rev() { |
| 607 | let hello = &peer.hello; | 684 | let hello = &peer.hello; |
| 608 | let picture = (self.peers.pictures) | 685 | let picture = (self.peers.pictures) |
| 609 | .entry(hello.peer) | 686 | .entry(hello.peer) |
| ... | @@ -615,6 +692,8 @@ impl State { | ... | @@ -615,6 +692,8 @@ impl State { |
| 615 | flags: Flags::CLICKABLE, | 692 | flags: Flags::CLICKABLE, |
| 616 | size: [px(AVATAR); 2], | 693 | size: [px(AVATAR); 2], |
| 617 | fill: Some(color(&hello.peer)), | 694 | fill: Some(color(&hello.peer)), |
| | 695 | // Parted from the avatar it overlaps by the bar's own colour. |
| | 696 | border: Some(self.ui.theme.strip), |
| 618 | radius: AVATAR / 2.0, | 697 | radius: AVATAR / 2.0, |
| 619 | pad: [2.0, 2.0], | 698 | pad: [2.0, 2.0], |
| 620 | role: Some(accesskit::Role::Button), | 699 | role: Some(accesskit::Role::Button), |
| ... | @@ -649,19 +728,70 @@ impl State { | ... | @@ -649,19 +728,70 @@ impl State { |
| 649 | } | 728 | } |
| 650 | let place = self.place_of(peer); | 729 | let place = self.place_of(peer); |
| 651 | ui::popup::tooltip(&mut self.ui, &hello.name, "", place.as_deref()); | 730 | ui::popup::tooltip(&mut self.ui, &hello.name, "", place.as_deref()); |
| 652 | if self.ui.signal(avatar).clicked | 731 | if self.ui.signal(avatar).clicked { |
| 653 | && let Some(session) = &self.session | 732 | chosen = Some(hello.peer); |
| 654 | && let Some(presence) = peer.presence.as_ref() | | |
| 655 | && presence.section == section(session) | | |
| 656 | && let Some(page) = presence.page | | |
| 657 | { | | |
| 658 | self.commands.push(Command::OpenPage(page.into())); | | |
| 659 | } | 733 | } |
| 660 | } | 734 | } |
| 661 | if self.live_options.presence { | 735 | if self.live_options.presence { |
| 662 | self.seen(); | 736 | self.seen(); |
| 663 | } | 737 | } |
| 664 | self.ui.close(); | 738 | self.ui.close(); |
| | 739 | if let Some(peer) = chosen { |
| | 740 | self.peers.going = Some((peer, now, None)); |
| | 741 | } |
| | 742 | } |
| | 743 | |
| | 744 | /// Goes to where the peer an avatar's click chose is: their section, their page, then |
| | 745 | /// their caret or selection in the middle of the view; given up after a few seconds. |
| | 746 | fn go_to_peer(&mut self) -> Option<()> { |
| | 747 | let (id, since, asked) = self.peers.going?; |
| | 748 | if since.elapsed() > std::time::Duration::from_secs(10) { |
| | 749 | self.peers.going = None; |
| | 750 | return None; |
| | 751 | } |
| | 752 | let peer = self |
| | 753 | .connected() |
| | 754 | .into_iter() |
| | 755 | .find(|peer| peer.hello.peer == id)?; |
| | 756 | let presence = peer.presence?; |
| | 757 | let session = self.session.as_ref()?; |
| | 758 | let place = (presence.section, presence.page); |
| | 759 | if presence.section != section(session) { |
| | 760 | let tab = (session.tabs.iter()) |
| | 761 | .find(|tab| session.library.section_identity(&tab.path) == presence.section)?; |
| | 762 | if asked != Some(place) { |
| | 763 | let library = Arc::clone(&session.library); |
| | 764 | self.commands |
| | 765 | .push(Command::OpenSection(library, tab.path.clone())); |
| | 766 | } |
| | 767 | } else if presence.page != Some(session.space.into()) { |
| | 768 | if asked != Some(place) { |
| | 769 | self.commands.push(Command::OpenPage(presence.page?.into())); |
| | 770 | } |
| | 771 | } else { |
| | 772 | self.peers.going = None; |
| | 773 | let caret = presence.caret?; |
| | 774 | let (outline, focus) = find(&self.view.editor, caret.focus)?; |
| | 775 | let rect = outline |
| | 776 | .caret_at(focus, parley::Affinity::Downstream, 0.0) |
| | 777 | .ok()?; |
| | 778 | let [x, y] = outline.origin(); |
| | 779 | let viewport = self.view.viewport; |
| | 780 | let [width, height] = viewport.size.map(|side| side as f32); |
| | 781 | let middle = ((rect.y0 + rect.y1) as f32 / 2.0 + y) * viewport.scale; |
| | 782 | let mut responses = vec![self.view.scroll_to(1, middle - height / 2.0).ok()?]; |
| | 783 | let across = (rect.x0 as f32 + x) * viewport.scale + viewport.origin[0]; |
| | 784 | if !(0.0..width).contains(&across) { |
| | 785 | let left = (rect.x0 as f32 + x) * viewport.scale - width / 2.0; |
| | 786 | responses.push(self.view.scroll_to(0, left).ok()?); |
| | 787 | } |
| | 788 | for response in responses { |
| | 789 | self.respond(response); |
| | 790 | } |
| | 791 | return Some(()); |
| | 792 | } |
| | 793 | self.peers.going = Some((id, since, Some(place))); |
| | 794 | Some(()) |
| 665 | } | 795 | } |
| 666 | | 796 | |
| 667 | /// The mark beside the avatars that says the others see this window too, which opens | 797 | /// The mark beside the avatars that says the others see this window too, which opens |