| ... | ... | @@ -25,6 +25,8 @@ use ui::{Flags, Id, Spec, children, fill, fit, px}; |
| 25 | 25 | /// The side of the picture sent, in pixels. |
| 26 | 26 | const PICTURE: u32 = 96; |
| 27 | 27 | const AVATAR: f32 = 22.0; |
| 28 | /// The avatars shown before the rest bundle into a chip. |
| 29 | const STACKED: usize = 3; |
| 28 | 30 | /// The name flag above a caret, and how long it shows after the caret last moved. |
| 29 | 31 | const FLAG: f32 = 15.0; |
| 30 | 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 | 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 | 271 | /// What a thread of its own sends the frame, and where the frame takes it. |
| 263 | 272 | pub(crate) type Channel<T> = (mpsc::Sender<T>, mpsc::Receiver<T>); |
| 264 | 273 | |
| ... | ... | @@ -286,6 +295,10 @@ pub(crate) struct Peers { |
| 286 | 295 | pictures: HashMap<[u8; 16], Option<draw::RasterImage>>, |
| 287 | 296 | /// Where each peer's caret was last seen, and when it got there. |
| 288 | 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 | 302 | pub(crate) share: Option<crate::share::ShareDialog>, |
| 290 | 303 | pub(crate) join: Option<crate::share::JoinDialog>, |
| 291 | 304 | } |
| ... | ... | @@ -295,6 +308,7 @@ impl State { |
| 295 | 308 | /// window is: in the shown notebook's room, and in each Live Share's. |
| 296 | 309 | pub(crate) fn follow_peers(&mut self) { |
| 297 | 310 | self.follow_shares(); |
| 311 | self.go_to_peer(); |
| 298 | 312 | let shown = self |
| 299 | 313 | .session |
| 300 | 314 | .as_ref() |
| ... | ... | @@ -587,23 +601,86 @@ impl State { |
| 587 | 601 | pages |
| 588 | 602 | } |
| 589 | 603 | |
| 590 | | /// The others with the notebook open, as avatars leftward from the search box: a click |
| 591 | | /// opens the page someone has open in this section. |
| 604 | /// The others with the notebook open, as avatars leftward from the search box, the most |
| 605 | /// lately active nearest it: past `STACKED` the rest are a chip that lists everyone. A |
| 606 | /// click goes to where someone is. |
| 592 | 607 | pub(crate) fn avatars(&mut self) { |
| 593 | | let peers = self.connected(); |
| 608 | let mut peers = self.connected(); |
| 594 | 609 | if peers.is_empty() { |
| 595 | 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 | 631 | self.ui.open( |
| 598 | 632 | "peers", |
| 599 | 633 | Spec { |
| 600 | 634 | size: [children(), px(TAB_ROW)], |
| 601 | 635 | pad: [6.0, (TAB_ROW - AVATAR) / 2.0], |
| 602 | | gap: 4.0, |
| 636 | gap: -6.0, |
| 603 | 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 | 684 | let hello = &peer.hello; |
| 608 | 685 | let picture = (self.peers.pictures) |
| 609 | 686 | .entry(hello.peer) |
| ... | ... | @@ -615,6 +692,8 @@ impl State { |
| 615 | 692 | flags: Flags::CLICKABLE, |
| 616 | 693 | size: [px(AVATAR); 2], |
| 617 | 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 | 697 | radius: AVATAR / 2.0, |
| 619 | 698 | pad: [2.0, 2.0], |
| 620 | 699 | role: Some(accesskit::Role::Button), |
| ... | ... | @@ -649,19 +728,70 @@ impl State { |
| 649 | 728 | } |
| 650 | 729 | let place = self.place_of(peer); |
| 651 | 730 | ui::popup::tooltip(&mut self.ui, &hello.name, "", place.as_deref()); |
| 652 | | if self.ui.signal(avatar).clicked |
| 653 | | && let Some(session) = &self.session |
| 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())); |
| 731 | if self.ui.signal(avatar).clicked { |
| 732 | chosen = Some(hello.peer); |
| 659 | 733 | } |
| 660 | 734 | } |
| 661 | 735 | if self.live_options.presence { |
| 662 | 736 | self.seen(); |
| 663 | 737 | } |
| 664 | 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 | 797 | /// The mark beside the avatars that says the others see this window too, which opens |