| ... | ... | @@ -20,13 +20,15 @@ use std::{ |
| 20 | 20 | path::{Path, PathBuf}, |
| 21 | 21 | sync::{Arc, Mutex, OnceLock, Weak, mpsc}, |
| 22 | 22 | }; |
| 23 | | use ui::{Flags, Spec, children, fill, fit, px}; |
| 23 | use ui::{Flags, Id, Spec, children, fill, fit, px}; |
| 24 | 24 | |
| 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 name flag above a caret. |
| 28 | /// The name flag above a caret, and how long it shows after the caret last moved. |
| 29 | 29 | const FLAG: f32 = 15.0; |
| 30 | const FLAG_PAD: f32 = 4.0; |
| 31 | const NAMED: std::time::Duration = std::time::Duration::from_secs(3); |
| 30 | 32 | /// The relay used where Options names none. |
| 31 | 33 | pub(crate) const DEFAULT_RELAY: &str = "wss://relay.snowbound.paperclover.net"; |
| 32 | 34 | /// How long opening a notebook joined for the first time waits for the computer sharing it. |
| ... | ... | @@ -263,6 +265,8 @@ pub(crate) struct Peers { |
| 263 | 265 | reached: HashMap<String, bool>, |
| 264 | 266 | /// Each peer's picture as drawn, cut to a circle, decoded once. |
| 265 | 267 | pictures: HashMap<[u8; 16], Option<draw::RasterImage>>, |
| 268 | /// Where each peer's caret was last seen, and when it got there. |
| 269 | moved: HashMap<[u8; 16], (Caret, std::time::Instant)>, |
| 266 | 270 | pub(crate) share: Option<crate::share::ShareDialog>, |
| 267 | 271 | pub(crate) join: Option<crate::share::JoinDialog>, |
| 268 | 272 | } |
| ... | ... | @@ -768,8 +772,39 @@ impl State { |
| 768 | 772 | if !(0.0..right - left).contains(&x0) || y1 < 0.0 || y0 > bottom - top { |
| 769 | 773 | continue; |
| 770 | 774 | } |
| 775 | let id = peer.hello.peer; |
| 776 | let now = std::time::Instant::now(); |
| 777 | let moved = match self.peers.moved.get(&id) { |
| 778 | Some((at, when)) if *at == caret => *when, |
| 779 | _ => { |
| 780 | self.peers.moved.insert(id, (caret, now)); |
| 781 | now |
| 782 | } |
| 783 | }; |
| 784 | let quiet = now.saturating_duration_since(moved); |
| 785 | if quiet < NAMED { |
| 786 | self.ui.wake_after(NAMED - quiet); |
| 787 | } |
| 788 | let name = (peer.hello.name.split_whitespace().next()).unwrap_or("Someone"); |
| 789 | // Below the caret where the page's top would cut the flag off. |
| 790 | let below = y0 < FLAG; |
| 791 | let flag_top = if below { y1 } else { y0 - FLAG }; |
| 792 | // About the flag's width, for hovering it. |
| 793 | let width = FLAG_PAD * 2.0 + self.ui.measure(name)[0] * 10.0 / self.ui.theme.font_size; |
| 794 | let hovered = self.ui.pointer().is_some_and(|[px, py]| { |
| 795 | let [px, py] = [px - left, py - top]; |
| 796 | (x0 - 4.0..=x0 + 4.0).contains(&px) && (y0..=y1).contains(&py) |
| 797 | || (x0 - 1.0..=x0 - 1.0 + width).contains(&px) |
| 798 | && (flag_top..=flag_top + FLAG).contains(&py) |
| 799 | }); |
| 800 | let shown_flag = self.ui.animate( |
| 801 | Id::ROOT.child((id, "flag-shown")), |
| 802 | if quiet < NAMED || hovered { 1.0 } else { 0.0 }, |
| 803 | ); |
| 804 | let faded = |
| 805 | |[red, green, blue, alpha]: [f32; 4], by: f32| [red, green, blue, alpha * by]; |
| 771 | 806 | self.ui.leaf( |
| 772 | | (peer.hello.peer, "caret"), |
| 807 | (id, "caret"), |
| 773 | 808 | Spec { |
| 774 | 809 | flags: Flags::FLOAT, |
| 775 | 810 | position: [x0 - 1.0, y0], |
| ... | ... | @@ -778,28 +813,47 @@ impl State { |
| 778 | 813 | ..Spec::default() |
| 779 | 814 | }, |
| 780 | 815 | ); |
| 781 | | let name = peer |
| 782 | | .hello |
| 783 | | .name |
| 784 | | .split_whitespace() |
| 785 | | .next() |
| 786 | | .unwrap_or("Someone"); |
| 816 | // The caret's cap, where its name is not shown. |
| 787 | 817 | self.ui.leaf( |
| 788 | | (peer.hello.peer, "flag"), |
| 818 | (id, "cap"), |
| 789 | 819 | Spec { |
| 790 | 820 | flags: Flags::FLOAT, |
| 791 | | position: [x0 - 1.0, y0 - FLAG], |
| 792 | | size: [fit(), px(FLAG)], |
| 793 | | text: Some(name), |
| 794 | | font_size: Some(10.0), |
| 795 | | bold: true, |
| 796 | | color: Some([1.0; 4]), |
| 797 | | fill: Some(color), |
| 821 | position: [x0 - 3.0, if below { y1 - 3.0 } else { y0 - 3.0 }], |
| 822 | size: [px(6.0), px(6.0)], |
| 823 | fill: Some(faded(color, 1.0 - shown_flag)), |
| 798 | 824 | radius: 3.0, |
| 799 | | pad: [4.0, 0.0], |
| 800 | 825 | ..Spec::default() |
| 801 | 826 | }, |
| 802 | 827 | ); |
| 828 | if shown_flag > 0.0 { |
| 829 | self.ui.leaf( |
| 830 | (id, "flag"), |
| 831 | Spec { |
| 832 | flags: Flags::FLOAT, |
| 833 | position: [x0 - 1.0, flag_top], |
| 834 | size: [fit(), px(FLAG)], |
| 835 | text: Some(name), |
| 836 | font_size: Some(10.0), |
| 837 | bold: true, |
| 838 | color: Some(faded([1.0; 4], shown_flag)), |
| 839 | fill: Some(faded(color, shown_flag)), |
| 840 | radius: 3.0, |
| 841 | pad: [FLAG_PAD, 0.0], |
| 842 | ..Spec::default() |
| 843 | }, |
| 844 | ); |
| 845 | // The corner on the caret square, so flag and caret are one shape. |
| 846 | self.ui.leaf( |
| 847 | (id, "joint"), |
| 848 | Spec { |
| 849 | flags: Flags::FLOAT, |
| 850 | position: [x0 - 1.0, if below { y1 } else { y0 - 3.0 }], |
| 851 | size: [px(3.0), px(3.0)], |
| 852 | fill: Some(faded(color, shown_flag)), |
| 853 | ..Spec::default() |
| 854 | }, |
| 855 | ); |
| 856 | } |
| 803 | 857 | } |
| 804 | 858 | } |
| 805 | 859 | } |