refactor(editor): move the collab join field onto the unified text input
The join field was a bare String with a hand-rolled whole-field-selection flag and no caret at all. It now rides TextInputState + TextInputView like every other chrome input: blinking caret, real selection highlight, and forward deletion for free. The field joins the active_text_input resolver so the caret blink drives the shared redraw wake-up, and the bespoke join_address_selected flag (a repeated source of stale-selection bugs) is gone.
This commit is contained in:
parent
8654c04b00
commit
ecebc3e748
|
|
@ -10,9 +10,8 @@ impl fmt::Debug for CollabPanelState {
|
|||
.debug_struct("CollabPanelState")
|
||||
.field("open", &self.open)
|
||||
.field("view", &self.view)
|
||||
.field("join_address", &"[REDACTED]")
|
||||
.field("join_input", &"[REDACTED]")
|
||||
.field("join_address_focused", &self.join_address_focused)
|
||||
.field("join_address_selected", &self.join_address_selected)
|
||||
.field("hover", &self.hover)
|
||||
.field("discovered", &self.discovered)
|
||||
.finish()
|
||||
|
|
@ -58,10 +57,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn join_address_is_redacted_from_debug() {
|
||||
let panel = CollabPanelState {
|
||||
join_address: "opc1_secret-route-capability".to_owned(),
|
||||
..CollabPanelState::default()
|
||||
};
|
||||
let mut panel = CollabPanelState::default();
|
||||
panel.join_input.set_text("opc1_secret-route-capability");
|
||||
let debug = format!("{panel:?}");
|
||||
assert!(debug.contains("[REDACTED]"));
|
||||
assert!(!debug.contains("secret-route-capability"));
|
||||
|
|
|
|||
|
|
@ -106,15 +106,14 @@ pub struct DiscoveredCollabEndpoint {
|
|||
pub compatible: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Default)]
|
||||
#[derive(Clone, PartialEq, Default)]
|
||||
pub struct CollabPanelState {
|
||||
pub open: bool,
|
||||
pub view: CollabPanelView,
|
||||
pub join_address: String,
|
||||
/// The invite-or-`host:port` field, on the unified single-line input
|
||||
/// component (caret, blink, selection, click-to-position all shared).
|
||||
pub join_input: jian_core::text_input::TextInputState,
|
||||
pub join_address_focused: bool,
|
||||
/// Whole-field selection for the plain-string join field (Cmd/Ctrl+A).
|
||||
/// The next typed character or Backspace replaces/clears the field.
|
||||
pub join_address_selected: bool,
|
||||
pub hover: Option<CollabPanelHover>,
|
||||
pub discovered: Arc<Vec<DiscoveredCollabEndpoint>>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,11 +91,13 @@ impl EditorUiState {
|
|||
&& self.collab.panel.join_address_focused
|
||||
}
|
||||
|
||||
/// Drop even a stale Join-field focus bit when another surface takes over.
|
||||
/// The whole-field selection dies with the focus so a later refocus never
|
||||
/// resurrects a destructive replace-on-type state.
|
||||
/// Drop even a stale Join-field focus bit when another surface takes
|
||||
/// over. The input's selection collapses with it so a later refocus
|
||||
/// never resurrects a destructive replace-on-type state.
|
||||
pub fn blur_collab_join_input(&mut self) -> bool {
|
||||
self.collab.panel.join_address_selected = false;
|
||||
let input = &mut self.collab.panel.join_input;
|
||||
let end = input.text().len();
|
||||
input.set_caret(end, 0);
|
||||
std::mem::take(&mut self.collab.panel.join_address_focused)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ impl EditorState {
|
|||
if self.editor_ui.chat_model_picker.open {
|
||||
return Some(&self.editor_ui.chat_model_picker_input);
|
||||
}
|
||||
// The collaboration Join field rides the visibility-aware focus
|
||||
// predicate so a stale bit never claims blink/clipboard ownership.
|
||||
if self.editor_ui.collab_join_input_active() {
|
||||
return Some(&self.editor_ui.collab.panel.join_input);
|
||||
}
|
||||
if self.chat.focused {
|
||||
return Some(&self.chat.input);
|
||||
}
|
||||
|
|
@ -128,6 +133,9 @@ impl EditorState {
|
|||
if self.editor_ui.chat_model_picker.open {
|
||||
return Some(&mut self.editor_ui.chat_model_picker_input);
|
||||
}
|
||||
if self.editor_ui.collab_join_input_active() {
|
||||
return Some(&mut self.editor_ui.collab.panel.join_input);
|
||||
}
|
||||
if self.chat.focused {
|
||||
return Some(&mut self.chat.input);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,10 +80,18 @@ pub struct CollabPanel<'a> {
|
|||
ui: &'a EditorUiState,
|
||||
model: CollabPanelModel,
|
||||
theme: Theme,
|
||||
/// Frame clock for the join field's caret blink. Hit-test-only callers
|
||||
/// construct with 0 — geometry never depends on it.
|
||||
now_ms: u64,
|
||||
}
|
||||
|
||||
impl<'a> CollabPanel<'a> {
|
||||
pub fn for_editor_ui(ui: &'a EditorUiState) -> Option<Self> {
|
||||
Self::for_editor_ui_at(ui, 0)
|
||||
}
|
||||
|
||||
/// Build with a frame clock so the join field's caret blinks.
|
||||
pub fn for_editor_ui_at(ui: &'a EditorUiState, now_ms: u64) -> Option<Self> {
|
||||
if !ui.collab.panel.open {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -92,6 +100,7 @@ impl<'a> CollabPanel<'a> {
|
|||
ui,
|
||||
model: CollabPanelModel::for_editor_ui(ui),
|
||||
theme: theme_for(ui),
|
||||
now_ms,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -219,7 +228,7 @@ impl Widget for CollabPanel<'_> {
|
|||
self.paint_owner_confirmation(cx, rect, body_top, confirm);
|
||||
}
|
||||
CollabPanelScreen::Join {
|
||||
address,
|
||||
address: _,
|
||||
discovered,
|
||||
} => {
|
||||
paint_text(
|
||||
|
|
@ -253,42 +262,30 @@ impl Widget for CollabPanel<'_> {
|
|||
1.0,
|
||||
);
|
||||
let clear = self.clear_join_rect(rect, body_top + 22.0);
|
||||
let text_width = if clear.is_some() {
|
||||
input.size.x - 18.0 - CLEAR_BUTTON_SIZE
|
||||
let text_inset = if clear.is_some() {
|
||||
CLEAR_BUTTON_SIZE + 5.0
|
||||
} else {
|
||||
input.size.x - 18.0
|
||||
0.0
|
||||
};
|
||||
let shown = if address.is_empty() {
|
||||
op_i18n::translate(self.ui.locale, "collab.join.codePlaceholder").to_string()
|
||||
} else {
|
||||
crate::util::ellipsize_to_width(address, text_width, |text| {
|
||||
cx.backend.measure_text(text, 12.0)
|
||||
})
|
||||
};
|
||||
if !address.is_empty()
|
||||
&& self.ui.collab.panel.join_address_focused
|
||||
&& self.ui.collab.panel.join_address_selected
|
||||
{
|
||||
let selection = Rect::xywh(
|
||||
input.origin.x + 6.0,
|
||||
input.origin.y + 6.0,
|
||||
cx.backend.measure_text(&shown, 12.0).min(text_width) + 6.0,
|
||||
input.size.y - 12.0,
|
||||
);
|
||||
cx.backend
|
||||
.fill_round_rect(selection, 4.0, self.theme.ring.with_alpha(0.35));
|
||||
}
|
||||
paint_text(
|
||||
// Value, placeholder, selection highlight, and blinking caret
|
||||
// all render through the unified text-input view.
|
||||
let view_rect = Rect::xywh(
|
||||
input.origin.x,
|
||||
input.origin.y,
|
||||
input.size.x - text_inset,
|
||||
input.size.y,
|
||||
);
|
||||
crate::widgets::property_panel_text_input::paint_text_input_view(
|
||||
cx,
|
||||
&shown,
|
||||
&self.theme,
|
||||
&self.ui.collab.panel.join_input,
|
||||
view_rect,
|
||||
12.0,
|
||||
if address.is_empty() {
|
||||
self.theme.muted_foreground
|
||||
} else {
|
||||
self.theme.foreground
|
||||
},
|
||||
Point2D::new(input.origin.x + 9.0, input.origin.y + 21.0),
|
||||
400,
|
||||
9.0,
|
||||
input.origin.y + 21.0,
|
||||
self.now_ms,
|
||||
op_i18n::translate(self.ui.locale, "collab.join.codePlaceholder"),
|
||||
self.ui.collab.panel.join_address_focused,
|
||||
);
|
||||
if let Some(clear) = clear {
|
||||
if self.ui.collab.panel.hover == Some(CollabPanelHover::ClearJoinAddress) {
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ impl CollabPanel<'_> {
|
|||
/// Clear (×) affordance inside the join field. `None` while the field is
|
||||
/// empty so an idle input never paints or hit-tests a dead button.
|
||||
pub(super) fn clear_join_rect(&self, panel: Rect, y: f32) -> Option<Rect> {
|
||||
if self.ui.collab.panel.join_address.is_empty() {
|
||||
if self.ui.collab.panel.join_input.text().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let input = self.address_rect(panel, y);
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ fn join_controls_hover_only_when_they_are_interactive() {
|
|||
ui.collab.availability = CollabAvailability::Ready;
|
||||
ui.collab.panel.open = true;
|
||||
ui.collab.panel.view = CollabPanelView::Join;
|
||||
ui.collab.panel.join_address = "opc1_public-invite".into();
|
||||
ui.collab.panel.join_input.set_text("opc1_public-invite");
|
||||
ui.collab.panel.discovered = Arc::new(vec![
|
||||
DiscoveredCollabEndpoint {
|
||||
discovery_id: "compatible".into(),
|
||||
|
|
@ -527,7 +527,7 @@ fn height_clamped_body_paint_is_clipped_above_action_row() {
|
|||
ui.collab.availability = CollabAvailability::Ready;
|
||||
ui.collab.panel.open = true;
|
||||
ui.collab.panel.view = CollabPanelView::Join;
|
||||
ui.collab.panel.join_address = "198.51.100.42:443".into();
|
||||
ui.collab.panel.join_input.set_text("198.51.100.42:443");
|
||||
let panel = CollabPanel::for_editor_ui(&ui).unwrap();
|
||||
let rect = panel.rect_at(
|
||||
Rect::xywh(600.0, 8.0, 100.0, 26.0),
|
||||
|
|
@ -720,7 +720,7 @@ fn join_clear_button_wins_inside_the_input_and_needs_content() {
|
|||
ui.collab.availability = CollabAvailability::Ready;
|
||||
ui.collab.panel.open = true;
|
||||
ui.collab.panel.view = CollabPanelView::Join;
|
||||
ui.collab.panel.join_address = "opc1_public-invite".into();
|
||||
ui.collab.panel.join_input.set_text("opc1_public-invite");
|
||||
|
||||
let panel = CollabPanel::for_editor_ui(&ui).unwrap();
|
||||
let rect = panel.rect_at(Rect::xywh(600.0, 8.0, 100.0, 26.0), viewport());
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ fn panel_session_or_pre_auth(
|
|||
|| collab.phase == CollabConnectionPhase::Discovering
|
||||
{
|
||||
let mut actions = Vec::new();
|
||||
let endpoint = collab.panel.join_address.trim();
|
||||
let endpoint = collab.panel.join_input.text().trim();
|
||||
if !endpoint.is_empty() {
|
||||
actions.push(CollabPanelActionModel {
|
||||
action: CollabUiAction::JoinAddress {
|
||||
|
|
@ -366,7 +366,7 @@ fn panel_session_or_pre_auth(
|
|||
actions.push(action_model(ui, CollabUiAction::Cancel, false));
|
||||
(
|
||||
CollabPanelScreen::Join {
|
||||
address: collab.panel.join_address.clone(),
|
||||
address: collab.panel.join_input.text().to_owned(),
|
||||
discovered: collab.panel.discovered.as_ref().clone(),
|
||||
},
|
||||
actions,
|
||||
|
|
@ -464,20 +464,20 @@ pub fn apply_panel_hit(
|
|||
CollabPanelHit::Close => {
|
||||
ui.collab.panel.open = false;
|
||||
ui.collab.panel.join_address_focused = false;
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
ui.collab.panel.hover = None;
|
||||
true
|
||||
}
|
||||
CollabPanelHit::FocusJoinAddress => {
|
||||
// A plain click focuses with a collapsed caret; it never keeps a
|
||||
// stale whole-field selection alive.
|
||||
// A plain click focuses with a collapsed caret at the end; it
|
||||
// never keeps a stale whole-field selection alive.
|
||||
ui.collab.panel.join_address_focused = true;
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
let input = &mut ui.collab.panel.join_input;
|
||||
let end = input.text().len();
|
||||
input.set_caret(end, 0);
|
||||
true
|
||||
}
|
||||
CollabPanelHit::ClearJoinAddress => {
|
||||
ui.collab.panel.join_address.clear();
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
ui.collab.panel.join_input.set_text("");
|
||||
ui.collab.panel.join_address_focused = true;
|
||||
ui.collab.panel.hover = None;
|
||||
true
|
||||
|
|
@ -490,7 +490,6 @@ pub fn apply_panel_hit(
|
|||
ui.login_modal_hover = None;
|
||||
ui.collab.panel.open = false;
|
||||
ui.collab.panel.join_address_focused = false;
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
ui.collab.panel.hover = None;
|
||||
true
|
||||
}
|
||||
|
|
@ -500,28 +499,22 @@ pub fn apply_panel_hit(
|
|||
CollabPanelHit::CopyInvite(_) => false,
|
||||
CollabPanelHit::Inside => {
|
||||
ui.collab.panel.join_address_focused = false;
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
true
|
||||
}
|
||||
CollabPanelHit::Action(CollabUiAction::OpenCreate) => {
|
||||
ui.collab.panel.view = op_editor_core::CollabPanelView::Create;
|
||||
ui.collab.panel.join_address_focused = false;
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
ui.collab.panel.hover = None;
|
||||
true
|
||||
}
|
||||
CollabPanelHit::Action(CollabUiAction::OpenJoin) => {
|
||||
ui.collab.panel.view = op_editor_core::CollabPanelView::Join;
|
||||
ui.collab.panel.join_address_focused = true;
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
ui.collab.panel.hover = None;
|
||||
true
|
||||
}
|
||||
CollabPanelHit::Action(CollabUiAction::BeginDiscovery) => {
|
||||
ui.collab.panel.view = op_editor_core::CollabPanelView::Join;
|
||||
// Find-nearby keeps the field focused; a surviving whole-field
|
||||
// selection would make the next keystroke destructive.
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
request_action(ui, CollabUiAction::BeginDiscovery)
|
||||
}
|
||||
CollabPanelHit::Action(CollabUiAction::Cancel)
|
||||
|
|
@ -529,13 +522,11 @@ pub fn apply_panel_hit(
|
|||
{
|
||||
ui.collab.panel.view = op_editor_core::CollabPanelView::Home;
|
||||
ui.collab.panel.join_address_focused = false;
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
ui.collab.panel.hover = None;
|
||||
true
|
||||
}
|
||||
CollabPanelHit::Action(action) => {
|
||||
ui.collab.panel.join_address_focused = false;
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
request_action(ui, action)
|
||||
}
|
||||
}
|
||||
|
|
@ -543,27 +534,25 @@ pub fn apply_panel_hit(
|
|||
|
||||
/// Typed-character routing for the invite-or-`host:port` field. `None` means
|
||||
/// the collaboration input is not focused; `Some` means it owns the key.
|
||||
pub fn join_address_text(ui: &mut EditorUiState, character: char) -> Option<bool> {
|
||||
pub fn join_address_text(ui: &mut EditorUiState, character: char, now_ms: u64) -> Option<bool> {
|
||||
if !ui.collab.panel.join_address_focused {
|
||||
return None;
|
||||
}
|
||||
if character.is_control() {
|
||||
if character.is_control() || !join_address_char_allowed(character) {
|
||||
return Some(false);
|
||||
}
|
||||
if !join_address_char_allowed(character) {
|
||||
let input = &mut ui.collab.panel.join_input;
|
||||
// The cap applies to the post-edit length: typing over a selection must
|
||||
// still be able to replace a full field.
|
||||
let selected = input
|
||||
.highlight_range()
|
||||
.map(|(start, end)| end - start)
|
||||
.unwrap_or(0);
|
||||
if input.text().chars().count() - selected >= MAX_JOIN_TARGET_CHARS {
|
||||
return Some(false);
|
||||
}
|
||||
// A whole-field selection replaces on type, like every range-selection
|
||||
// input: the first accepted character clears the old value. The length
|
||||
// cap below deliberately runs AFTER the take — a full field must still
|
||||
// be replaceable by typing over the selection.
|
||||
if std::mem::take(&mut ui.collab.panel.join_address_selected) {
|
||||
ui.collab.panel.join_address.clear();
|
||||
}
|
||||
if ui.collab.panel.join_address.chars().count() >= MAX_JOIN_TARGET_CHARS {
|
||||
return Some(false);
|
||||
}
|
||||
ui.collab.panel.join_address.push(character);
|
||||
let mut buffer = [0_u8; 4];
|
||||
input.insert_str(character.encode_utf8(&mut buffer), now_ms);
|
||||
ui.collab.panel.hover = None;
|
||||
Some(true)
|
||||
}
|
||||
|
|
@ -575,19 +564,29 @@ fn join_address_char_allowed(character: char) -> bool {
|
|||
character.is_ascii_alphanumeric() || matches!(character, '.' | ':' | '-' | '[' | ']' | '_')
|
||||
}
|
||||
|
||||
pub fn join_address_backspace(ui: &mut EditorUiState) -> Option<bool> {
|
||||
pub fn join_address_backspace(ui: &mut EditorUiState, now_ms: u64) -> Option<bool> {
|
||||
if !ui.collab.panel.join_address_focused {
|
||||
return None;
|
||||
}
|
||||
if std::mem::take(&mut ui.collab.panel.join_address_selected) {
|
||||
let changed = !ui.collab.panel.join_address.is_empty();
|
||||
ui.collab.panel.join_address.clear();
|
||||
if changed {
|
||||
ui.collab.panel.hover = None;
|
||||
}
|
||||
return Some(changed);
|
||||
let input = &mut ui.collab.panel.join_input;
|
||||
let before = input.text().to_owned();
|
||||
input.backspace(now_ms);
|
||||
let changed = input.text() != before;
|
||||
if changed {
|
||||
ui.collab.panel.hover = None;
|
||||
}
|
||||
let changed = ui.collab.panel.join_address.pop().is_some();
|
||||
Some(changed)
|
||||
}
|
||||
|
||||
/// Forward deletion (the Delete key) on the focused join field.
|
||||
pub fn join_address_delete_forward(ui: &mut EditorUiState, now_ms: u64) -> Option<bool> {
|
||||
if !ui.collab.panel.join_address_focused {
|
||||
return None;
|
||||
}
|
||||
let input = &mut ui.collab.panel.join_input;
|
||||
let before = input.text().to_owned();
|
||||
input.delete_forward(now_ms);
|
||||
let changed = input.text() != before;
|
||||
if changed {
|
||||
ui.collab.panel.hover = None;
|
||||
}
|
||||
|
|
@ -596,19 +595,23 @@ pub fn join_address_backspace(ui: &mut EditorUiState) -> Option<bool> {
|
|||
|
||||
/// Cmd/Ctrl+A on the focused join field — whole-field selection. `None`
|
||||
/// means the field is not focused and the chord belongs to someone else.
|
||||
pub fn join_address_select_all(ui: &mut EditorUiState) -> Option<bool> {
|
||||
pub fn join_address_select_all(ui: &mut EditorUiState, now_ms: u64) -> Option<bool> {
|
||||
if !ui.collab.panel.join_address_focused {
|
||||
return None;
|
||||
}
|
||||
let selectable = !ui.collab.panel.join_address.is_empty();
|
||||
ui.collab.panel.join_address_selected = selectable;
|
||||
let input = &mut ui.collab.panel.join_input;
|
||||
let selectable = !input.text().is_empty();
|
||||
if selectable {
|
||||
input.select_all();
|
||||
input.touch(now_ms);
|
||||
}
|
||||
Some(selectable)
|
||||
}
|
||||
|
||||
/// Clipboard paste into the focused join field. Replaces the whole field —
|
||||
/// an invite code is pasted as a unit, and append semantics silently
|
||||
/// produced corrupt old+new concatenations. `None` means not focused.
|
||||
pub fn join_address_paste(ui: &mut EditorUiState, text: &str) -> Option<bool> {
|
||||
pub fn join_address_paste(ui: &mut EditorUiState, text: &str, now_ms: u64) -> Option<bool> {
|
||||
if !ui.collab.panel.join_address_focused {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -620,8 +623,9 @@ pub fn join_address_paste(ui: &mut EditorUiState, text: &str) -> Option<bool> {
|
|||
if sanitized.is_empty() {
|
||||
return Some(false);
|
||||
}
|
||||
ui.collab.panel.join_address = sanitized;
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
let input = &mut ui.collab.panel.join_input;
|
||||
input.set_text(sanitized);
|
||||
input.touch(now_ms);
|
||||
ui.collab.panel.hover = None;
|
||||
Some(true)
|
||||
}
|
||||
|
|
@ -630,7 +634,7 @@ pub fn join_address_submit(ui: &mut EditorUiState) -> Option<bool> {
|
|||
if !ui.collab.panel.join_address_focused {
|
||||
return None;
|
||||
}
|
||||
let endpoint = ui.collab.panel.join_address.trim();
|
||||
let endpoint = ui.collab.panel.join_input.text().trim();
|
||||
if endpoint.is_empty() {
|
||||
return Some(false);
|
||||
}
|
||||
|
|
@ -638,7 +642,6 @@ pub fn join_address_submit(ui: &mut EditorUiState) -> Option<bool> {
|
|||
endpoint: endpoint.to_string(),
|
||||
};
|
||||
ui.collab.panel.join_address_focused = false;
|
||||
ui.collab.panel.join_address_selected = false;
|
||||
ui.collab.panel.hover = None;
|
||||
Some(request_action(ui, action))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ fn join_model_exposes_only_discovery_endpoint_data() {
|
|||
let mut ui = EditorUiState::default();
|
||||
ui.collab.availability = CollabAvailability::Ready;
|
||||
ui.collab.panel.view = CollabPanelView::Join;
|
||||
ui.collab.panel.join_address = "10.0.0.2:43120".to_string();
|
||||
ui.collab.panel.join_input.set_text("10.0.0.2:43120");
|
||||
ui.collab.panel.discovered = std::sync::Arc::new(vec![DiscoveredCollabEndpoint {
|
||||
discovery_id: "opaque-1".to_string(),
|
||||
endpoint: "10.0.0.3:43120".to_string(),
|
||||
|
|
|
|||
|
|
@ -108,9 +108,9 @@ fn invite_or_address_input_is_bounded_and_queues_one_join() {
|
|||
let mut ui = EditorUiState::default();
|
||||
ui.collab.panel.join_address_focused = true;
|
||||
for character in target.chars() {
|
||||
assert_eq!(join_address_text(&mut ui, character), Some(true));
|
||||
assert_eq!(join_address_text(&mut ui, character, 0), Some(true));
|
||||
}
|
||||
assert_eq!(join_address_text(&mut ui, ' '), Some(false));
|
||||
assert_eq!(join_address_text(&mut ui, ' ', 0), Some(false));
|
||||
assert_eq!(join_address_submit(&mut ui), Some(true));
|
||||
assert_eq!(
|
||||
ui.collab.take_pending_action(),
|
||||
|
|
@ -127,7 +127,7 @@ fn join_target_and_action_debug_are_redacted() {
|
|||
let mut ui = EditorUiState::default();
|
||||
ui.collab.availability = CollabAvailability::Ready;
|
||||
ui.collab.panel.view = CollabPanelView::Join;
|
||||
ui.collab.panel.join_address = raw_invite.into();
|
||||
ui.collab.panel.join_input.set_text(raw_invite);
|
||||
|
||||
let model = CollabPanelModel::for_editor_ui(&ui);
|
||||
let debug = format!("{model:?}");
|
||||
|
|
@ -273,40 +273,42 @@ fn conflict_notice_names_the_discarded_fields_and_offers_reapply() {
|
|||
fn paste_replaces_the_whole_join_field() {
|
||||
let mut ui = EditorUiState::default();
|
||||
ui.collab.panel.join_address_focused = true;
|
||||
ui.collab.panel.join_address = "opc1_stale-old-code".into();
|
||||
ui.collab.panel.join_input.set_text("opc1_stale-old-code");
|
||||
|
||||
assert_eq!(join_address_paste(&mut ui, "opc1_fresh_code\n"), Some(true));
|
||||
assert_eq!(ui.collab.panel.join_address, "opc1_fresh_code");
|
||||
assert_eq!(
|
||||
join_address_paste(&mut ui, "opc1_fresh_code\n", 0),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(ui.collab.panel.join_input.text(), "opc1_fresh_code");
|
||||
|
||||
// Whitespace-only payloads change nothing rather than clearing the field.
|
||||
assert_eq!(join_address_paste(&mut ui, " \n\t"), Some(false));
|
||||
assert_eq!(ui.collab.panel.join_address, "opc1_fresh_code");
|
||||
assert_eq!(join_address_paste(&mut ui, " \n\t", 0), Some(false));
|
||||
assert_eq!(ui.collab.panel.join_input.text(), "opc1_fresh_code");
|
||||
|
||||
ui.collab.panel.join_address_focused = false;
|
||||
assert_eq!(join_address_paste(&mut ui, "opc1_x"), None);
|
||||
assert_eq!(join_address_paste(&mut ui, "opc1_x", 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_all_then_backspace_clears_and_type_replaces() {
|
||||
let mut ui = EditorUiState::default();
|
||||
ui.collab.panel.join_address_focused = true;
|
||||
ui.collab.panel.join_address = "opc1_very-long-invite".into();
|
||||
ui.collab.panel.join_input.set_text("opc1_very-long-invite");
|
||||
|
||||
assert_eq!(join_address_select_all(&mut ui), Some(true));
|
||||
assert!(ui.collab.panel.join_address_selected);
|
||||
assert_eq!(join_address_backspace(&mut ui), Some(true));
|
||||
assert!(ui.collab.panel.join_address.is_empty());
|
||||
assert!(!ui.collab.panel.join_address_selected);
|
||||
assert_eq!(join_address_select_all(&mut ui, 0), Some(true));
|
||||
assert!(ui.collab.panel.join_input.highlight_range().is_some());
|
||||
assert_eq!(join_address_backspace(&mut ui, 0), Some(true));
|
||||
assert!(ui.collab.panel.join_input.text().is_empty());
|
||||
|
||||
// Select-all on an empty field selects nothing.
|
||||
assert_eq!(join_address_select_all(&mut ui), Some(false));
|
||||
assert!(!ui.collab.panel.join_address_selected);
|
||||
assert_eq!(join_address_select_all(&mut ui, 0), Some(false));
|
||||
assert!(ui.collab.panel.join_input.highlight_range().is_none());
|
||||
|
||||
ui.collab.panel.join_address = "opc1_old".into();
|
||||
assert_eq!(join_address_select_all(&mut ui), Some(true));
|
||||
assert_eq!(join_address_text(&mut ui, 'x'), Some(true));
|
||||
assert_eq!(ui.collab.panel.join_address, "x");
|
||||
assert!(!ui.collab.panel.join_address_selected);
|
||||
ui.collab.panel.join_input.set_text("opc1_old");
|
||||
assert_eq!(join_address_select_all(&mut ui, 0), Some(true));
|
||||
assert_eq!(join_address_text(&mut ui, 'x', 0), Some(true));
|
||||
assert_eq!(ui.collab.panel.join_input.text(), "x");
|
||||
assert!(ui.collab.panel.join_input.highlight_range().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -315,38 +317,41 @@ fn clear_hit_empties_the_field_and_keeps_focus() {
|
|||
ui.collab.availability = CollabAvailability::Ready;
|
||||
ui.collab.panel.open = true;
|
||||
ui.collab.panel.view = CollabPanelView::Join;
|
||||
ui.collab.panel.join_address = "opc1_something".into();
|
||||
ui.collab.panel.join_address_selected = true;
|
||||
ui.collab.panel.join_input.set_text("opc1_something");
|
||||
ui.collab.panel.join_input.select_all();
|
||||
|
||||
assert!(apply_panel_hit(
|
||||
&mut ui,
|
||||
crate::widgets::collab_panel::CollabPanelHit::ClearJoinAddress,
|
||||
));
|
||||
assert!(ui.collab.panel.join_address.is_empty());
|
||||
assert!(ui.collab.panel.join_input.text().is_empty());
|
||||
assert!(ui.collab.panel.join_address_focused);
|
||||
assert!(!ui.collab.panel.join_address_selected);
|
||||
assert!(ui.collab.panel.join_input.highlight_range().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blur_paths_drop_the_whole_field_selection() {
|
||||
fn refocus_by_click_collapses_a_stale_selection() {
|
||||
let mut ui = EditorUiState::default();
|
||||
ui.collab.panel.join_address_focused = true;
|
||||
ui.collab.panel.join_address = "opc1_abc".into();
|
||||
ui.collab.panel.join_address_selected = true;
|
||||
ui.collab.panel.join_input.set_text("opc1_abc");
|
||||
ui.collab.panel.join_input.select_all();
|
||||
|
||||
assert!(apply_panel_hit(
|
||||
&mut ui,
|
||||
crate::widgets::collab_panel::CollabPanelHit::Inside,
|
||||
));
|
||||
assert!(!ui.collab.panel.join_address_focused);
|
||||
assert!(!ui.collab.panel.join_address_selected);
|
||||
|
||||
// Re-focusing by click never resurrects a stale selection.
|
||||
ui.collab.panel.join_address_selected = true;
|
||||
// Re-focusing by click never resurrects a stale selection: the caret
|
||||
// collapses to the end of the buffer.
|
||||
assert!(apply_panel_hit(
|
||||
&mut ui,
|
||||
crate::widgets::collab_panel::CollabPanelHit::FocusJoinAddress,
|
||||
));
|
||||
assert!(ui.collab.panel.join_address_focused);
|
||||
assert!(!ui.collab.panel.join_address_selected);
|
||||
assert!(ui.collab.panel.join_input.highlight_range().is_none());
|
||||
assert_eq!(
|
||||
ui.collab.panel.join_input.caret(),
|
||||
ui.collab.panel.join_input.text().len()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ fn focus_join(host: &mut WidgetHostNative) {
|
|||
collab.panel.open = true;
|
||||
collab.panel.view = CollabPanelView::Join;
|
||||
collab.panel.join_address_focused = true;
|
||||
collab.panel.join_address.clear();
|
||||
collab.panel.join_input.set_text("");
|
||||
}
|
||||
|
||||
fn host_with_selected_node() -> WidgetHostNative {
|
||||
|
|
@ -52,17 +52,17 @@ fn join_field_owns_native_text_ime_paste_backspace_and_enter() {
|
|||
assert!(host.apply_input_paste("opc1_Ab-9\n"));
|
||||
assert!(host.apply_ime_commit("Z"));
|
||||
assert_eq!(
|
||||
host.editor_state().editor_ui.collab.panel.join_address,
|
||||
host.editor_state().editor_ui.collab.panel.join_input.text(),
|
||||
"opc1_Ab-9Z"
|
||||
);
|
||||
assert!(host.apply_text('/'), "rejected input is still consumed");
|
||||
assert_eq!(
|
||||
host.editor_state().editor_ui.collab.panel.join_address,
|
||||
host.editor_state().editor_ui.collab.panel.join_input.text(),
|
||||
"opc1_Ab-9Z"
|
||||
);
|
||||
assert!(host.apply_backspace());
|
||||
assert_eq!(
|
||||
host.editor_state().editor_ui.collab.panel.join_address,
|
||||
host.editor_state().editor_ui.collab.panel.join_input.text(),
|
||||
"opc1_Ab-9"
|
||||
);
|
||||
|
||||
|
|
@ -269,20 +269,21 @@ fn native_join_paste_replaces_and_select_all_clears() {
|
|||
assert!(host.apply_input_paste("opc1_first-code"));
|
||||
assert!(host.apply_input_paste("opc1_second-code"));
|
||||
assert_eq!(
|
||||
host.editor_state().editor_ui.collab.panel.join_address,
|
||||
host.editor_state().editor_ui.collab.panel.join_input.text(),
|
||||
"opc1_second-code",
|
||||
"a pasted invite replaces the stale one instead of appending"
|
||||
);
|
||||
|
||||
// Cmd/Ctrl+A owns the chord and selects the whole field...
|
||||
assert!(host.apply_select_all());
|
||||
assert!(
|
||||
host.editor_state()
|
||||
.editor_ui
|
||||
.collab
|
||||
.panel
|
||||
.join_address_selected
|
||||
);
|
||||
assert!(host
|
||||
.editor_state()
|
||||
.editor_ui
|
||||
.collab
|
||||
.panel
|
||||
.join_input
|
||||
.highlight_range()
|
||||
.is_some());
|
||||
// ...so one Backspace clears it.
|
||||
assert!(host.apply_backspace());
|
||||
assert!(host
|
||||
|
|
@ -290,7 +291,8 @@ fn native_join_paste_replaces_and_select_all_clears() {
|
|||
.editor_ui
|
||||
.collab
|
||||
.panel
|
||||
.join_address
|
||||
.join_input
|
||||
.text()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
|
|
@ -298,12 +300,11 @@ fn native_join_paste_replaces_and_select_all_clears() {
|
|||
fn native_join_delete_clears_selection_and_never_deletes_nodes() {
|
||||
let mut host = host_with_selected_node();
|
||||
let before = host.editor_state().active_children().len();
|
||||
host.editor_state_mut().editor_ui.collab.panel.join_address = "opc1_code".into();
|
||||
host.editor_state_mut()
|
||||
.editor_ui
|
||||
.collab
|
||||
.panel
|
||||
.join_address_selected = true;
|
||||
{
|
||||
let input = &mut host.editor_state_mut().editor_ui.collab.panel.join_input;
|
||||
input.set_text("opc1_code");
|
||||
input.select_all();
|
||||
}
|
||||
|
||||
assert!(host.apply_delete());
|
||||
assert!(host
|
||||
|
|
@ -311,7 +312,8 @@ fn native_join_delete_clears_selection_and_never_deletes_nodes() {
|
|||
.editor_ui
|
||||
.collab
|
||||
.panel
|
||||
.join_address
|
||||
.join_input
|
||||
.text()
|
||||
.is_empty());
|
||||
// Delete with an empty, unselected field is still swallowed.
|
||||
assert!(!host.apply_delete());
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ impl WidgetHostNative {
|
|||
let changed = op_editor_ui::widgets::collab_ui::join_address_text(
|
||||
&mut self.editor_state.editor_ui,
|
||||
c,
|
||||
self.now_ms,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if changed {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ impl WidgetHostNative {
|
|||
let changed = op_editor_ui::widgets::collab_ui::join_address_paste(
|
||||
&mut self.editor_state.editor_ui,
|
||||
text,
|
||||
self.now_ms,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if changed {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ impl WidgetHostNative {
|
|||
if self.editor_state.editor_ui.collab_join_input_active() {
|
||||
let changed = op_editor_ui::widgets::collab_ui::join_address_backspace(
|
||||
&mut self.editor_state.editor_ui,
|
||||
self.now_ms,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if changed {
|
||||
|
|
@ -267,25 +268,19 @@ impl WidgetHostNative {
|
|||
if self.apply_image_panel_delete() {
|
||||
return true;
|
||||
}
|
||||
// A whole-field selection in the join input makes Delete a clear.
|
||||
// Without one, `delete_owned_by_chrome_input` below still swallows
|
||||
// the key before it can reach the canvas selection.
|
||||
if self.editor_state.editor_ui.collab_join_input_active()
|
||||
&& self
|
||||
.editor_state
|
||||
.editor_ui
|
||||
.collab
|
||||
.panel
|
||||
.join_address_selected
|
||||
{
|
||||
let changed = op_editor_ui::widgets::collab_ui::join_address_backspace(
|
||||
// Forward deletion in the join input. A no-op falls through to
|
||||
// `delete_owned_by_chrome_input`, which still swallows the key
|
||||
// before it can reach the canvas selection.
|
||||
if self.editor_state.editor_ui.collab_join_input_active() {
|
||||
let changed = op_editor_ui::widgets::collab_ui::join_address_delete_forward(
|
||||
&mut self.editor_state.editor_ui,
|
||||
self.now_ms,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if changed {
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// The open font picker owns Delete. Its search draft handles
|
||||
// Backspace separately; forward-delete must never reach the canvas
|
||||
|
|
|
|||
|
|
@ -503,9 +503,10 @@ impl WidgetHostNative {
|
|||
// Collaboration popover — a real shared widget anchored to the
|
||||
// collaboration status chip. It consumes only sanitized UI state;
|
||||
// the native session actor drains queued actions separately.
|
||||
if let Some(panel) =
|
||||
op_editor_ui::widgets::CollabPanel::for_editor_ui(&self.editor_state.editor_ui)
|
||||
{
|
||||
if let Some(panel) = op_editor_ui::widgets::CollabPanel::for_editor_ui_at(
|
||||
&self.editor_state.editor_ui,
|
||||
self.now_ms,
|
||||
) {
|
||||
let anchor = top_bar.collaboration_chip_rect_estimated(top_bar_rect);
|
||||
let panel_rect = panel.rect_at(
|
||||
anchor,
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ impl WidgetHostNative {
|
|||
if self.editor_state.editor_ui.collab_join_input_active() {
|
||||
if op_editor_ui::widgets::collab_ui::join_address_select_all(
|
||||
&mut self.editor_state.editor_ui,
|
||||
self.now_ms,
|
||||
) == Some(true)
|
||||
{
|
||||
self.mark_dirty();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ fn focus_join(host: &mut WidgetHost) {
|
|||
collab.panel.open = true;
|
||||
collab.panel.view = CollabPanelView::Join;
|
||||
collab.panel.join_address_focused = true;
|
||||
collab.panel.join_address.clear();
|
||||
collab.panel.join_input.set_text("");
|
||||
}
|
||||
|
||||
fn host_with_selected_node() -> WidgetHost {
|
||||
|
|
@ -51,17 +51,17 @@ fn join_field_owns_web_text_ime_paste_backspace_and_enter() {
|
|||
let ime = crate::event::ime::composition_end("Z".to_string());
|
||||
assert!(host.apply_ime(&ime));
|
||||
assert_eq!(
|
||||
host.editor_state.editor_ui.collab.panel.join_address,
|
||||
host.editor_state.editor_ui.collab.panel.join_input.text(),
|
||||
"opc1_Ab-9Z"
|
||||
);
|
||||
assert!(host.apply_text('/'), "rejected input is still consumed");
|
||||
assert_eq!(
|
||||
host.editor_state.editor_ui.collab.panel.join_address,
|
||||
host.editor_state.editor_ui.collab.panel.join_input.text(),
|
||||
"opc1_Ab-9Z"
|
||||
);
|
||||
assert!(host.apply_backspace());
|
||||
assert_eq!(
|
||||
host.editor_state.editor_ui.collab.panel.join_address,
|
||||
host.editor_state.editor_ui.collab.panel.join_input.text(),
|
||||
"opc1_Ab-9"
|
||||
);
|
||||
|
||||
|
|
@ -258,7 +258,8 @@ fn web_join_clipboard_replaces_and_select_all_clears() {
|
|||
assert!(host.apply_clipboard_text("opc1_first-code"));
|
||||
assert!(host.apply_clipboard_text("opc1_second-code"));
|
||||
assert_eq!(
|
||||
host.editor_state.editor_ui.collab.panel.join_address, "opc1_second-code",
|
||||
host.editor_state.editor_ui.collab.panel.join_input.text(),
|
||||
"opc1_second-code",
|
||||
"a pasted invite replaces the stale one instead of appending"
|
||||
);
|
||||
|
||||
|
|
@ -266,24 +267,26 @@ fn web_join_clipboard_replaces_and_select_all_clears() {
|
|||
let ime = crate::event::ime::composition_end("Z".to_string());
|
||||
assert!(host.apply_ime(&ime));
|
||||
assert_eq!(
|
||||
host.editor_state.editor_ui.collab.panel.join_address,
|
||||
host.editor_state.editor_ui.collab.panel.join_input.text(),
|
||||
"opc1_second-codeZ"
|
||||
);
|
||||
|
||||
assert!(host.apply_select_all());
|
||||
assert!(
|
||||
host.editor_state
|
||||
.editor_ui
|
||||
.collab
|
||||
.panel
|
||||
.join_address_selected
|
||||
);
|
||||
assert!(host
|
||||
.editor_state
|
||||
.editor_ui
|
||||
.collab
|
||||
.panel
|
||||
.join_input
|
||||
.highlight_range()
|
||||
.is_some());
|
||||
assert!(host.apply_backspace());
|
||||
assert!(host
|
||||
.editor_state
|
||||
.editor_ui
|
||||
.collab
|
||||
.panel
|
||||
.join_address
|
||||
.join_input
|
||||
.text()
|
||||
.is_empty());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ impl WidgetHost {
|
|||
let changed = op_editor_ui::widgets::collab_ui::join_address_text(
|
||||
&mut self.editor_state.editor_ui,
|
||||
c,
|
||||
self.now_ms,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if changed {
|
||||
|
|
@ -130,6 +131,7 @@ impl WidgetHost {
|
|||
if self.editor_state.editor_ui.collab_join_input_active() {
|
||||
let changed = op_editor_ui::widgets::collab_ui::join_address_backspace(
|
||||
&mut self.editor_state.editor_ui,
|
||||
self.now_ms,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if changed {
|
||||
|
|
@ -338,25 +340,19 @@ impl WidgetHost {
|
|||
if self.apply_image_panel_delete() {
|
||||
return true;
|
||||
}
|
||||
// A whole-field selection in the join input makes Delete a clear.
|
||||
// Without one, `delete_owned_by_chrome_input` below still swallows
|
||||
// the key before it can reach the canvas selection.
|
||||
if self.editor_state.editor_ui.collab_join_input_active()
|
||||
&& self
|
||||
.editor_state
|
||||
.editor_ui
|
||||
.collab
|
||||
.panel
|
||||
.join_address_selected
|
||||
{
|
||||
let changed = op_editor_ui::widgets::collab_ui::join_address_backspace(
|
||||
// Forward deletion in the join input. A no-op falls through to
|
||||
// `delete_owned_by_chrome_input`, which still swallows the key
|
||||
// before it can reach the canvas selection.
|
||||
if self.editor_state.editor_ui.collab_join_input_active() {
|
||||
let changed = op_editor_ui::widgets::collab_ui::join_address_delete_forward(
|
||||
&mut self.editor_state.editor_ui,
|
||||
self.now_ms,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if changed {
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// The open font picker owns Delete. Its search draft handles
|
||||
// Backspace separately; forward-delete must never reach the canvas
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ impl WidgetHost {
|
|||
if self.editor_state.editor_ui.collab_join_input_active() {
|
||||
if op_editor_ui::widgets::collab_ui::join_address_select_all(
|
||||
&mut self.editor_state.editor_ui,
|
||||
self.now_ms,
|
||||
) == Some(true)
|
||||
{
|
||||
self.mark_dirty();
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ impl WidgetHost {
|
|||
let changed = op_editor_ui::widgets::collab_ui::join_address_paste(
|
||||
&mut self.editor_state.editor_ui,
|
||||
text,
|
||||
self.now_ms,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if changed {
|
||||
|
|
|
|||
|
|
@ -340,9 +340,10 @@ impl WidgetHost {
|
|||
// Shared collaboration popover. Web builds normally keep the
|
||||
// capability unavailable for M1, but the real surface is present for
|
||||
// future satellite hosts and never reaches native transport APIs.
|
||||
if let Some(panel) =
|
||||
op_editor_ui::widgets::CollabPanel::for_editor_ui(&self.editor_state.editor_ui)
|
||||
{
|
||||
if let Some(panel) = op_editor_ui::widgets::CollabPanel::for_editor_ui_at(
|
||||
&self.editor_state.editor_ui,
|
||||
self.now_ms,
|
||||
) {
|
||||
let top_bar =
|
||||
op_editor_ui::widgets::TopBar::for_editor_ui(&self.editor_state.editor_ui)
|
||||
.with_traffic_controls(false);
|
||||
|
|
|
|||
Loading…
Reference in a new issue