1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//! The "Settings > Collab" subtab: identity, username, action rate and
//! relay configuration. Joining a session lives with the graphs it creates:
//! the Graphs pane's join button.
use crate::Responses;
use crate::collab::{CollabConfig, SessionConn};
/// The inputs for [`collab_config`].
pub struct CollabSettings<'a> {
/// The user-editable, persisted configuration.
pub config: &'a mut CollabConfig,
/// This user's public identity, once generated.
pub peer_id: Option<&'a str>,
/// The endpoint's home relay(s) and their connection state (empty until
/// the collab runtime starts).
pub relays: &'a [(String, bool)],
}
/// The Collab settings subtab: identity, username, action rate and relay
/// configuration.
///
/// Holds a per-frame snapshot of the persisted [`CollabConfig`] plus the
/// user's displayable identity and relay status. Edits apply to the snapshot
/// in place, and the full updated [`CollabConfig`] is emitted as a payload
/// for the collab layer to apply.
#[derive(Clone, Debug, Default)]
pub struct CollabSettingsTab {
/// The editable configuration snapshot.
pub config: CollabConfig,
/// This user's public identity, as a displayable string, once minted.
pub peer_id: Option<String>,
/// The endpoint's home relay(s) and their connection state.
pub relays: Vec<(String, bool)>,
}
impl crate::widget::SettingsTab for CollabSettingsTab {
fn title(&self) -> &str {
"Collab"
}
fn ui(&mut self, ui: &mut egui::Ui) -> Responses {
let mut responses = Responses::default();
let before = self.config.clone();
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| {
let settings = CollabSettings {
config: &mut self.config,
peer_id: self.peer_id.as_deref(),
relays: &self.relays,
};
collab_config(settings, ui)
});
if self.config != before {
responses.push(None, self.config.clone());
}
responses
}
}
/// Render the collab configuration: the user's identity, their shared
/// username, the live-action send rate and the relay configuration/status.
pub fn collab_config(settings: CollabSettings, ui: &mut egui::Ui) {
let CollabSettings {
config,
peer_id,
relays,
} = settings;
let control_w = (ui.available_width() - 64.0).max(64.0);
egui::Grid::new("collab_config_grid")
.num_columns(2)
.spacing([8.0, 6.0])
.striped(true)
.show(ui, |ui| {
// The public identity peers see (and can allowlist).
ui.label("identity");
match peer_id {
Some(id) => {
let short: String = id.chars().take(8).collect();
if ui
.button(format!("{short}…"))
.on_hover_text(format!("copy full public key\n{id}"))
.clicked()
{
ui.ctx().copy_text(id.to_string());
}
}
None => {
ui.label(
egui::RichText::new("generated when first shared")
.italics()
.weak(),
);
}
}
ui.end_row();
// The username shared with session peers.
ui.label("username");
ui.add(
egui::TextEdit::singleline(&mut config.username)
.hint_text("anonymous")
.desired_width(control_w),
);
ui.end_row();
// The per-node-path send window for live actions.
ui.label("action rate");
ui.add(
egui::DragValue::new(&mut config.action_rate_ms)
.speed(1)
.range(0..=1000)
.suffix(" ms"),
)
.on_hover_text(
"minimum interval between live-action sends per node \
(drags, bangs); values written faster batch into one \
message and replay in order on peers. 0 sends every frame",
);
ui.end_row();
// Presence cursors.
ui.label("pointers");
ui.checkbox(&mut config.show_pointers, "show peer pointers")
.on_hover_text(
"show session peers' live pointers over shared graphs; \
your own pointer is shared regardless",
);
ui.end_row();
// The relay server assisting (and, for browser peers, carrying)
// connections. Empty = iroh's default n0 public relays.
ui.label("relay");
let relay_id = ui.id().with("collab_relay");
let mut relay = ui
.data(|d| d.get_temp::<String>(relay_id))
.unwrap_or_else(|| config.custom_relay.clone().unwrap_or_default());
ui.horizontal(|ui| {
let resp = ui.add(
egui::TextEdit::singleline(&mut relay)
.hint_text("n0 public relays (default)")
.desired_width((control_w - 48.0).max(48.0)),
);
resp.on_hover_text(
"a custom relay server URL (e.g. a self-hosted iroh-relay). \
Replaces n0's public infrastructure entirely: peers \
connect via invite tickets and the relay, with no \
third-party address lookup. Applies when the app \
restarts",
);
if ui
.button("reset")
.on_hover_text("use the default (n0 public) relays")
.clicked()
{
relay.clear();
}
let trimmed = relay.trim();
config.custom_relay = (!trimmed.is_empty()).then(|| trimmed.to_string());
});
ui.data_mut(|d| d.insert_temp(relay_id, relay));
ui.end_row();
// Live relay status, once the collab runtime is up: who this
// peer is routed through.
if !relays.is_empty() {
ui.label("");
ui.vertical(|ui| {
for (url, connected) in relays {
ui.horizontal(|ui| {
let (color, label) = if *connected {
(SessionConn::Live.color(), "connected")
} else {
(SessionConn::Degraded.color(), "disconnected")
};
super::status_dot(ui, color).on_hover_text(label);
ui.label(egui::RichText::new(url).weak());
});
}
});
ui.end_row();
}
});
}