| ... | ... | @@ -31,6 +31,10 @@ const STACKED: usize = 3; |
| 31 | 31 | const FLAG: f32 = 15.0; |
| 32 | 32 | const FLAG_PAD: f32 = 4.0; |
| 33 | 33 | const NAMED: std::time::Duration = std::time::Duration::from_secs(3); |
| 34 | /// The carets drawn on a page at once: those who moved last; and the names a chip of those |
| 35 | /// out of view lists. |
| 36 | const CARETS: usize = 5; |
| 37 | const NAMES: usize = 5; |
| 34 | 38 | /// The relay used where Options names none. |
| 35 | 39 | pub(crate) const DEFAULT_RELAY: &str = "wss://relay.snowbound.paperclover.net"; |
| 36 | 40 | /// How long opening a notebook joined for the first time waits for the computer sharing it. |
| ... | ... | @@ -863,8 +867,9 @@ impl State { |
| 863 | 867 | Some(format!("In {}", tab.name)) |
| 864 | 868 | } |
| 865 | 869 | |
| 866 | | /// Each peer's caret on the open page with a flag naming them, and what they have |
| 867 | | /// selected, as boxes floating in the page's box. |
| 870 | /// The carets of the peers on the open page who moved last, each with a flag naming |
| 871 | /// them, and what they have selected, as boxes floating in the page's box; those above |
| 872 | /// or below the view bundle into a chip at its edge. |
| 868 | 873 | pub(crate) fn peer_carets(&mut self) { |
| 869 | 874 | let Some(session) = &self.session else { |
| 870 | 875 | return; |
| ... | ... | @@ -882,9 +887,10 @@ impl State { |
| 882 | 887 | (y * viewport.scale + viewport.origin[1]) / scale, |
| 883 | 888 | ] |
| 884 | 889 | }; |
| 890 | let now = std::time::Instant::now(); |
| 891 | let (mut placed, mut above, mut below) = (Vec::new(), Vec::new(), Vec::new()); |
| 885 | 892 | for peer in self.connected() { |
| 886 | | let Some(caret) = peer |
| 887 | | .presence |
| 893 | let Some(caret) = (peer.presence.as_ref()) |
| 888 | 894 | .filter(|presence| (presence.section, presence.page) == here) |
| 889 | 895 | .and_then(|presence| presence.caret) |
| 890 | 896 | else { |
| ... | ... | @@ -893,39 +899,13 @@ impl State { |
| 893 | 899 | let Some((outline, focus)) = find(&self.view.editor, caret.focus) else { |
| 894 | 900 | continue; |
| 895 | 901 | }; |
| 896 | | let color = color(&peer.hello.peer); |
| 897 | | let [x, y] = outline.origin(); |
| 898 | | if let Some((_, anchor)) = |
| 899 | | find(&self.view.editor, caret.anchor).filter(|(other, _)| other.id == outline.id) |
| 900 | | { |
| 901 | | let rects = outline |
| 902 | | .range_rects([anchor, focus].into()) |
| 903 | | .unwrap_or_default(); |
| 904 | | for (index, rect) in rects.into_iter().enumerate() { |
| 905 | | let [x0, y0] = shown([rect.x0 as f32 + x, rect.y0 as f32 + y]); |
| 906 | | let [x1, y1] = shown([rect.x1 as f32 + x, rect.y1 as f32 + y]); |
| 907 | | self.ui.leaf( |
| 908 | | (peer.hello.peer, "selection", index), |
| 909 | | Spec { |
| 910 | | flags: Flags::FLOAT, |
| 911 | | position: [x0, y0], |
| 912 | | size: [px(x1 - x0), px(y1 - y0)], |
| 913 | | fill: Some([color[0], color[1], color[2], 0.25]), |
| 914 | | ..Spec::default() |
| 915 | | }, |
| 916 | | ); |
| 917 | | } |
| 918 | | } |
| 919 | 902 | let Ok(rect) = outline.caret_at(focus, parley::Affinity::Downstream, 0.0) else { |
| 920 | 903 | continue; |
| 921 | 904 | }; |
| 905 | let [x, y] = outline.origin(); |
| 922 | 906 | let [x0, y0] = shown([rect.x0 as f32 + x, rect.y0 as f32 + y]); |
| 923 | 907 | let [_, y1] = shown([rect.x0 as f32 + x, rect.y1 as f32 + y]); |
| 924 | | if !(0.0..right - left).contains(&x0) || y1 < 0.0 || y0 > bottom - top { |
| 925 | | continue; |
| 926 | | } |
| 927 | 908 | let id = peer.hello.peer; |
| 928 | | let now = std::time::Instant::now(); |
| 929 | 909 | let moved = match self.peers.moved.get(&id) { |
| 930 | 910 | Some((at, when)) if *at == caret => *when, |
| 931 | 911 | _ => { |
| ... | ... | @@ -933,79 +913,178 @@ impl State { |
| 933 | 913 | now |
| 934 | 914 | } |
| 935 | 915 | }; |
| 936 | | let quiet = now.saturating_duration_since(moved); |
| 937 | | if quiet < NAMED { |
| 938 | | self.ui.wake_after(NAMED - quiet); |
| 916 | if y1 < 0.0 { |
| 917 | above.push((moved, peer)); |
| 918 | continue; |
| 939 | 919 | } |
| 940 | | let name = (peer.hello.name.split_whitespace().next()).unwrap_or("Someone"); |
| 941 | | // Below the caret where the page's top would cut the flag off. |
| 942 | | let below = y0 < FLAG; |
| 943 | | let flag_top = if below { y1 } else { y0 - FLAG }; |
| 944 | | // About the flag's width, for hovering it. |
| 945 | | let width = FLAG_PAD * 2.0 + self.ui.measure(name)[0] * 10.0 / self.ui.theme.font_size; |
| 946 | | let hovered = self.ui.pointer().is_some_and(|[px, py]| { |
| 947 | | let [px, py] = [px - left, py - top]; |
| 948 | | (x0 - 4.0..=x0 + 4.0).contains(&px) && (y0..=y1).contains(&py) |
| 949 | | || (x0 - 1.0..=x0 - 1.0 + width).contains(&px) |
| 950 | | && (flag_top..=flag_top + FLAG).contains(&py) |
| 951 | | }); |
| 952 | | let shown_flag = self.ui.animate( |
| 953 | | Id::ROOT.child((id, "flag-shown")), |
| 954 | | if quiet < NAMED || hovered { 1.0 } else { 0.0 }, |
| 920 | if y0 > bottom - top { |
| 921 | below.push((moved, peer)); |
| 922 | continue; |
| 923 | } |
| 924 | if !(0.0..right - left).contains(&x0) { |
| 925 | continue; |
| 926 | } |
| 927 | let selection = find(&self.view.editor, caret.anchor) |
| 928 | .filter(|(other, _)| other.id == outline.id) |
| 929 | .and_then(|(_, anchor)| outline.range_rects([anchor, focus].into()).ok()) |
| 930 | .unwrap_or_default() |
| 931 | .into_iter() |
| 932 | .map(|rect| { |
| 933 | let [x0, y0] = shown([rect.x0 as f32 + x, rect.y0 as f32 + y]); |
| 934 | let [x1, y1] = shown([rect.x1 as f32 + x, rect.y1 as f32 + y]); |
| 935 | [x0, y0, x1, y1] |
| 936 | }) |
| 937 | .collect::<Vec<_>>(); |
| 938 | placed.push((moved, peer, [x0, y0, y1], selection)); |
| 939 | } |
| 940 | placed.sort_by_key(|(moved, ..)| std::cmp::Reverse(*moved)); |
| 941 | for (moved, peer, at, selection) in placed.into_iter().take(CARETS) { |
| 942 | self.peer_caret(&peer, now.saturating_duration_since(moved), at, &selection); |
| 943 | } |
| 944 | let mut chosen = None; |
| 945 | for (edge, mut bundle) in [("above", above), ("below", below)] { |
| 946 | if bundle.is_empty() { |
| 947 | continue; |
| 948 | } |
| 949 | bundle.sort_by_key(|(moved, _)| std::cmp::Reverse(*moved)); |
| 950 | let label = format!("{} {edge}", bundle.len()); |
| 951 | let width = 16.0 + self.ui.measure(&label)[0] * 10.0 / self.ui.theme.font_size; |
| 952 | let y = if edge == "above" { |
| 953 | 6.0 |
| 954 | } else { |
| 955 | bottom - top - FLAG - 6.0 |
| 956 | }; |
| 957 | let chip = self.ui.leaf( |
| 958 | ("peers", edge), |
| 959 | Spec { |
| 960 | flags: Flags::FLOAT | Flags::CLICKABLE, |
| 961 | position: [(right - left - width) / 2.0, y], |
| 962 | size: [fit(), px(FLAG + 2.0)], |
| 963 | text: Some(&label), |
| 964 | font_size: Some(10.0), |
| 965 | bold: true, |
| 966 | center: true, |
| 967 | color: Some([1.0; 4]), |
| 968 | fill: Some(color(&bundle[0].1.hello.peer)), |
| 969 | radius: (FLAG + 2.0) / 2.0, |
| 970 | pad: [8.0, 0.0], |
| 971 | role: Some(accesskit::Role::Button), |
| 972 | ..Spec::default() |
| 973 | }, |
| 955 | 974 | ); |
| 956 | | let faded = |
| 957 | | |[red, green, blue, alpha]: [f32; 4], by: f32| [red, green, blue, alpha * by]; |
| 975 | let mut names: Vec<&str> = (bundle.iter().take(NAMES)) |
| 976 | .map(|(_, peer)| peer.hello.name.as_str()) |
| 977 | .collect(); |
| 978 | let more = format!("and {} more", bundle.len().saturating_sub(NAMES)); |
| 979 | if bundle.len() > NAMES { |
| 980 | names.push(&more); |
| 981 | } |
| 982 | ui::popup::tooltip(&mut self.ui, &names.join(", "), "", None); |
| 983 | if chip.clicked { |
| 984 | chosen = Some(bundle[0].1.hello.peer); |
| 985 | } |
| 986 | } |
| 987 | if let Some(peer) = chosen { |
| 988 | self.peers.going = Some((peer, now, None)); |
| 989 | } |
| 990 | } |
| 991 | |
| 992 | /// `peer`'s caret at `[x, top, bottom]` in the page's box, quiet for `quiet`, with what |
| 993 | /// they have selected. |
| 994 | fn peer_caret( |
| 995 | &mut self, |
| 996 | peer: &Peer, |
| 997 | quiet: std::time::Duration, |
| 998 | [x0, y0, y1]: [f32; 3], |
| 999 | selection: &[[f32; 4]], |
| 1000 | ) { |
| 1001 | let Some([left, top, ..]) = self.ui.rect(page()) else { |
| 1002 | return; |
| 1003 | }; |
| 1004 | let id = peer.hello.peer; |
| 1005 | let color = color(&id); |
| 1006 | for (index, [x0, y0, x1, y1]) in selection.iter().enumerate() { |
| 958 | 1007 | self.ui.leaf( |
| 959 | | (id, "caret"), |
| 1008 | (id, "selection", index), |
| 960 | 1009 | Spec { |
| 961 | 1010 | flags: Flags::FLOAT, |
| 962 | | position: [x0 - 1.0, y0], |
| 963 | | size: [px(2.0), px(y1 - y0)], |
| 964 | | fill: Some(color), |
| 1011 | position: [*x0, *y0], |
| 1012 | size: [px(x1 - x0), px(y1 - y0)], |
| 1013 | fill: Some([color[0], color[1], color[2], 0.25]), |
| 965 | 1014 | ..Spec::default() |
| 966 | 1015 | }, |
| 967 | 1016 | ); |
| 968 | | // The caret's cap, where its name is not shown. |
| 1017 | } |
| 1018 | if quiet < NAMED { |
| 1019 | self.ui.wake_after(NAMED - quiet); |
| 1020 | } |
| 1021 | let name = (peer.hello.name.split_whitespace().next()).unwrap_or("Someone"); |
| 1022 | // Below the caret where the page's top would cut the flag off. |
| 1023 | let below = y0 < FLAG; |
| 1024 | let flag_top = if below { y1 } else { y0 - FLAG }; |
| 1025 | // About the flag's width, for hovering it. |
| 1026 | let width = FLAG_PAD * 2.0 + self.ui.measure(name)[0] * 10.0 / self.ui.theme.font_size; |
| 1027 | let hovered = self.ui.pointer().is_some_and(|[px, py]| { |
| 1028 | let [px, py] = [px - left, py - top]; |
| 1029 | (x0 - 4.0..=x0 + 4.0).contains(&px) && (y0..=y1).contains(&py) |
| 1030 | || (x0 - 1.0..=x0 - 1.0 + width).contains(&px) |
| 1031 | && (flag_top..=flag_top + FLAG).contains(&py) |
| 1032 | }); |
| 1033 | let shown_flag = self.ui.animate( |
| 1034 | Id::ROOT.child((id, "flag-shown")), |
| 1035 | if quiet < NAMED || hovered { 1.0 } else { 0.0 }, |
| 1036 | ); |
| 1037 | let faded = |[red, green, blue, alpha]: [f32; 4], by: f32| [red, green, blue, alpha * by]; |
| 1038 | self.ui.leaf( |
| 1039 | (id, "caret"), |
| 1040 | Spec { |
| 1041 | flags: Flags::FLOAT, |
| 1042 | position: [x0 - 1.0, y0], |
| 1043 | size: [px(2.0), px(y1 - y0)], |
| 1044 | fill: Some(color), |
| 1045 | ..Spec::default() |
| 1046 | }, |
| 1047 | ); |
| 1048 | // The caret's cap, where its name is not shown. |
| 1049 | self.ui.leaf( |
| 1050 | (id, "cap"), |
| 1051 | Spec { |
| 1052 | flags: Flags::FLOAT, |
| 1053 | position: [x0 - 3.0, if below { y1 - 3.0 } else { y0 - 3.0 }], |
| 1054 | size: [px(6.0), px(6.0)], |
| 1055 | fill: Some(faded(color, 1.0 - shown_flag)), |
| 1056 | radius: 3.0, |
| 1057 | ..Spec::default() |
| 1058 | }, |
| 1059 | ); |
| 1060 | if shown_flag > 0.0 { |
| 969 | 1061 | self.ui.leaf( |
| 970 | | (id, "cap"), |
| 1062 | (id, "flag"), |
| 971 | 1063 | Spec { |
| 972 | 1064 | flags: Flags::FLOAT, |
| 973 | | position: [x0 - 3.0, if below { y1 - 3.0 } else { y0 - 3.0 }], |
| 974 | | size: [px(6.0), px(6.0)], |
| 975 | | fill: Some(faded(color, 1.0 - shown_flag)), |
| 1065 | position: [x0 - 1.0, flag_top], |
| 1066 | size: [fit(), px(FLAG)], |
| 1067 | text: Some(name), |
| 1068 | font_size: Some(10.0), |
| 1069 | bold: true, |
| 1070 | color: Some(faded([1.0; 4], shown_flag)), |
| 1071 | fill: Some(faded(color, shown_flag)), |
| 976 | 1072 | radius: 3.0, |
| 1073 | pad: [FLAG_PAD, 0.0], |
| 1074 | ..Spec::default() |
| 1075 | }, |
| 1076 | ); |
| 1077 | // The corner on the caret square, so flag and caret are one shape. |
| 1078 | self.ui.leaf( |
| 1079 | (id, "joint"), |
| 1080 | Spec { |
| 1081 | flags: Flags::FLOAT, |
| 1082 | position: [x0 - 1.0, if below { y1 } else { y0 - 3.0 }], |
| 1083 | size: [px(3.0), px(3.0)], |
| 1084 | fill: Some(faded(color, shown_flag)), |
| 977 | 1085 | ..Spec::default() |
| 978 | 1086 | }, |
| 979 | 1087 | ); |
| 980 | | if shown_flag > 0.0 { |
| 981 | | self.ui.leaf( |
| 982 | | (id, "flag"), |
| 983 | | Spec { |
| 984 | | flags: Flags::FLOAT, |
| 985 | | position: [x0 - 1.0, flag_top], |
| 986 | | size: [fit(), px(FLAG)], |
| 987 | | text: Some(name), |
| 988 | | font_size: Some(10.0), |
| 989 | | bold: true, |
| 990 | | color: Some(faded([1.0; 4], shown_flag)), |
| 991 | | fill: Some(faded(color, shown_flag)), |
| 992 | | radius: 3.0, |
| 993 | | pad: [FLAG_PAD, 0.0], |
| 994 | | ..Spec::default() |
| 995 | | }, |
| 996 | | ); |
| 997 | | // The corner on the caret square, so flag and caret are one shape. |
| 998 | | self.ui.leaf( |
| 999 | | (id, "joint"), |
| 1000 | | Spec { |
| 1001 | | flags: Flags::FLOAT, |
| 1002 | | position: [x0 - 1.0, if below { y1 } else { y0 - 3.0 }], |
| 1003 | | size: [px(3.0), px(3.0)], |
| 1004 | | fill: Some(faded(color, shown_flag)), |
| 1005 | | ..Spec::default() |
| 1006 | | }, |
| 1007 | | ); |
| 1008 | | } |
| 1009 | 1088 | } |
| 1010 | 1089 | } |
| 1011 | 1090 | } |