| ... | @@ -31,6 +31,10 @@ const STACKED: usize = 3; | ... | @@ -31,6 +31,10 @@ const STACKED: usize = 3; |
| 31 | const FLAG: f32 = 15.0; | 31 | const FLAG: f32 = 15.0; |
| 32 | const FLAG_PAD: f32 = 4.0; | 32 | const FLAG_PAD: f32 = 4.0; |
| 33 | const NAMED: std::time::Duration = std::time::Duration::from_secs(3); | 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 | /// The relay used where Options names none. | 38 | /// The relay used where Options names none. |
| 35 | pub(crate) const DEFAULT_RELAY: &str = "wss://relay.snowbound.paperclover.net"; | 39 | pub(crate) const DEFAULT_RELAY: &str = "wss://relay.snowbound.paperclover.net"; |
| 36 | /// How long opening a notebook joined for the first time waits for the computer sharing it. | 40 | /// How long opening a notebook joined for the first time waits for the computer sharing it. |
| ... | @@ -863,8 +867,9 @@ impl State { | ... | @@ -863,8 +867,9 @@ impl State { |
| 863 | Some(format!("In {}", tab.name)) | 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 | 870 | /// The carets of the peers on the open page who moved last, each with a flag naming |
| 867 | /// selected, as boxes floating in the page's box. | 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 | pub(crate) fn peer_carets(&mut self) { | 873 | pub(crate) fn peer_carets(&mut self) { |
| 869 | let Some(session) = &self.session else { | 874 | let Some(session) = &self.session else { |
| 870 | return; | 875 | return; |
| ... | @@ -882,9 +887,10 @@ impl State { | ... | @@ -882,9 +887,10 @@ impl State { |
| 882 | (y * viewport.scale + viewport.origin[1]) / scale, | 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 | for peer in self.connected() { | 892 | for peer in self.connected() { |
| 886 | let Some(caret) = peer | 893 | let Some(caret) = (peer.presence.as_ref()) |
| 887 | .presence | | |
| 888 | .filter(|presence| (presence.section, presence.page) == here) | 894 | .filter(|presence| (presence.section, presence.page) == here) |
| 889 | .and_then(|presence| presence.caret) | 895 | .and_then(|presence| presence.caret) |
| 890 | else { | 896 | else { |
| ... | @@ -893,39 +899,13 @@ impl State { | ... | @@ -893,39 +899,13 @@ impl State { |
| 893 | let Some((outline, focus)) = find(&self.view.editor, caret.focus) else { | 899 | let Some((outline, focus)) = find(&self.view.editor, caret.focus) else { |
| 894 | continue; | 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 | let Ok(rect) = outline.caret_at(focus, parley::Affinity::Downstream, 0.0) else { | 902 | let Ok(rect) = outline.caret_at(focus, parley::Affinity::Downstream, 0.0) else { |
| 920 | continue; | 903 | continue; |
| 921 | }; | 904 | }; |
| | 905 | let [x, y] = outline.origin(); |
| 922 | let [x0, y0] = shown([rect.x0 as f32 + x, rect.y0 as f32 + y]); | 906 | let [x0, y0] = shown([rect.x0 as f32 + x, rect.y0 as f32 + y]); |
| 923 | let [_, y1] = shown([rect.x0 as f32 + x, rect.y1 as f32 + y]); | 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 | let id = peer.hello.peer; | 908 | let id = peer.hello.peer; |
| 928 | let now = std::time::Instant::now(); | | |
| 929 | let moved = match self.peers.moved.get(&id) { | 909 | let moved = match self.peers.moved.get(&id) { |
| 930 | Some((at, when)) if *at == caret => *when, | 910 | Some((at, when)) if *at == caret => *when, |
| 931 | _ => { | 911 | _ => { |
| ... | @@ -933,79 +913,178 @@ impl State { | ... | @@ -933,79 +913,178 @@ impl State { |
| 933 | now | 913 | now |
| 934 | } | 914 | } |
| 935 | }; | 915 | }; |
| 936 | let quiet = now.saturating_duration_since(moved); | 916 | if y1 < 0.0 { |
| 937 | if quiet < NAMED { | 917 | above.push((moved, peer)); |
| 938 | self.ui.wake_after(NAMED - quiet); | 918 | continue; |
| 939 | } | 919 | } |
| 940 | let name = (peer.hello.name.split_whitespace().next()).unwrap_or("Someone"); | 920 | if y0 > bottom - top { |
| 941 | // Below the caret where the page's top would cut the flag off. | 921 | below.push((moved, peer)); |
| 942 | let below = y0 < FLAG; | 922 | continue; |
| 943 | let flag_top = if below { y1 } else { y0 - FLAG }; | 923 | } |
| 944 | // About the flag's width, for hovering it. | 924 | if !(0.0..right - left).contains(&x0) { |
| 945 | let width = FLAG_PAD * 2.0 + self.ui.measure(name)[0] * 10.0 / self.ui.theme.font_size; | 925 | continue; |
| 946 | let hovered = self.ui.pointer().is_some_and(|[px, py]| { | 926 | } |
| 947 | let [px, py] = [px - left, py - top]; | 927 | let selection = find(&self.view.editor, caret.anchor) |
| 948 | (x0 - 4.0..=x0 + 4.0).contains(&px) && (y0..=y1).contains(&py) | 928 | .filter(|(other, _)| other.id == outline.id) |
| 949 | || (x0 - 1.0..=x0 - 1.0 + width).contains(&px) | 929 | .and_then(|(_, anchor)| outline.range_rects([anchor, focus].into()).ok()) |
| 950 | && (flag_top..=flag_top + FLAG).contains(&py) | 930 | .unwrap_or_default() |
| 951 | }); | 931 | .into_iter() |
| 952 | let shown_flag = self.ui.animate( | 932 | .map(|rect| { |
| 953 | Id::ROOT.child((id, "flag-shown")), | 933 | let [x0, y0] = shown([rect.x0 as f32 + x, rect.y0 as f32 + y]); |
| 954 | if quiet < NAMED || hovered { 1.0 } else { 0.0 }, | 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 = | 975 | let mut names: Vec<&str> = (bundle.iter().take(NAMES)) |
| 957 | |[red, green, blue, alpha]: [f32; 4], by: f32| [red, green, blue, alpha * by]; | 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 | self.ui.leaf( | 1007 | self.ui.leaf( |
| 959 | (id, "caret"), | 1008 | (id, "selection", index), |
| 960 | Spec { | 1009 | Spec { |
| 961 | flags: Flags::FLOAT, | 1010 | flags: Flags::FLOAT, |
| 962 | position: [x0 - 1.0, y0], | 1011 | position: [*x0, *y0], |
| 963 | size: [px(2.0), px(y1 - y0)], | 1012 | size: [px(x1 - x0), px(y1 - y0)], |
| 964 | fill: Some(color), | 1013 | fill: Some([color[0], color[1], color[2], 0.25]), |
| 965 | ..Spec::default() | 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 | self.ui.leaf( | 1061 | self.ui.leaf( |
| 970 | (id, "cap"), | 1062 | (id, "flag"), |
| 971 | Spec { | 1063 | Spec { |
| 972 | flags: Flags::FLOAT, | 1064 | flags: Flags::FLOAT, |
| 973 | position: [x0 - 3.0, if below { y1 - 3.0 } else { y0 - 3.0 }], | 1065 | position: [x0 - 1.0, flag_top], |
| 974 | size: [px(6.0), px(6.0)], | 1066 | size: [fit(), px(FLAG)], |
| 975 | fill: Some(faded(color, 1.0 - shown_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 | radius: 3.0, | 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 | ..Spec::default() | 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 | } |