this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: keyboard nav preserves cursor column in destination card

ArrowDown/Up at segment boundary now lands cursor on the correct line
and column of the destination card (first line for Down, last for Up)
instead of always placing at the end.

Uses a nav_cursor signal to pass (col, going_up) from on_keydown to
onmounted, avoiding the previous setTimeout race with Dioxus re-render.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+44 -17
+44 -17
crates/tala/src/editor.rs
··· 956 956 let mut insert_error = use_signal(|| Option::<String>::None); 957 957 let mut selected_rect_sig = use_signal(|| Option::<usize>::None); 958 958 let mut edge_drag_active = use_signal(|| false); 959 + // Some((col, going_up)): desired cursor column set by keyboard nav just before 960 + // active_idx changes. Consumed by the new card's onmounted handler. 961 + let mut nav_cursor: Signal<Option<(usize, bool)>> = use_signal(|| None); 959 962 960 963 // Update glyph signals when the active card or its render result changes. 961 964 use_effect(move || { ··· 1049 1052 let cur_idx = *active_idx.read(); 1050 1053 let n = make_segments(&source.read().clone()).len(); 1051 1054 let mut ai = active_idx; 1055 + let mut nc = nav_cursor; 1052 1056 spawn(async move { 1053 - // Each branch: if on the boundary line → send true (navigate). 1054 - // Otherwise move the cursor one line and send false. 1057 + // Returns -1 if not at boundary (cursor moved within card). 1058 + // Returns col (>=0) if at boundary — the column to preserve in the 1059 + // destination card. 1055 1060 let js = if going_up { 1056 1061 "var t=document.querySelector('.card-row.active textarea');\ 1057 - if(!t){dioxus.send(false);return;}\ 1062 + if(!t){dioxus.send(-1);return;}\ 1058 1063 var pos=t.selectionStart;\ 1059 1064 var before=t.value.slice(0,pos);\ 1060 1065 var prevNl=before.lastIndexOf('\\n');\ 1061 - if(prevNl===-1){dioxus.send(true);}\ 1062 - else{\ 1066 + if(prevNl===-1){\ 1067 + var col=pos;\ 1068 + dioxus.send(col);\ 1069 + }else{\ 1063 1070 var col=pos-(prevNl+1);\ 1064 1071 var prevPrevNl=before.lastIndexOf('\\n',prevNl-1);\ 1065 1072 var newPos=Math.min(prevPrevNl+1+col,prevNl);\ 1066 1073 t.setSelectionRange(newPos,newPos);\ 1067 - dioxus.send(false);\ 1074 + dioxus.send(-1);\ 1068 1075 }" 1069 1076 } else { 1070 1077 "var t=document.querySelector('.card-row.active textarea');\ 1071 - if(!t){dioxus.send(false);return;}\ 1078 + if(!t){dioxus.send(-1);return;}\ 1072 1079 var pos=t.selectionStart;\ 1073 1080 var after=t.value.slice(pos);\ 1074 1081 var nextNl=after.indexOf('\\n');\ 1075 - if(nextNl===-1){dioxus.send(true);}\ 1076 - else{\ 1082 + if(nextNl===-1){\ 1083 + var before=t.value.slice(0,pos);\ 1084 + var col=pos-(before.lastIndexOf('\\n')+1);\ 1085 + dioxus.send(col);\ 1086 + }else{\ 1077 1087 var before=t.value.slice(0,pos);\ 1078 1088 var col=pos-(before.lastIndexOf('\\n')+1);\ 1079 1089 var nextLineStart=pos+nextNl+1;\ ··· 1081 1091 var nextNextNl=afterNext.indexOf('\\n');\ 1082 1092 var nextLineEnd=nextNextNl===-1?t.value.length:nextLineStart+nextNextNl;\ 1083 1093 t.setSelectionRange(Math.min(nextLineStart+col,nextLineEnd),Math.min(nextLineStart+col,nextLineEnd));\ 1084 - dioxus.send(false);\ 1094 + dioxus.send(-1);\ 1085 1095 }" 1086 1096 }; 1087 - if let Ok(at_boundary) = document::eval(js).recv::<bool>().await { 1088 - if at_boundary { 1097 + if let Ok(col) = document::eval(js).recv::<i64>().await { 1098 + if col >= 0 { 1089 1099 let new_i = if going_up { 1090 1100 cur_idx.saturating_sub(1) 1091 1101 } else { 1092 1102 (cur_idx + 1).min(n.saturating_sub(1)) 1093 1103 }; 1094 1104 if new_i != cur_idx { 1105 + // Store nav intent BEFORE re-render so onmounted picks it up. 1106 + nc.set(Some((col as usize, going_up))); 1095 1107 ai.set(new_i); 1096 - document::eval( 1097 - "setTimeout(()=>document.querySelector('.card-row.active textarea')?.focus(),0)" 1098 - ).await.ok(); 1099 1108 } 1100 1109 } 1101 1110 } ··· 1409 1418 let frag = frag_for_mount; 1410 1419 move |_| { 1411 1420 let b64 = base64::engine::general_purpose::STANDARD.encode(&frag); 1421 + // Consume nav_cursor if set by keyboard nav. 1422 + let nav = nav_cursor.write_unchecked(); 1423 + let cursor_js = if let Some((col, going_up)) = *nav { 1424 + if going_up { 1425 + format!("var lastNl=t.value.lastIndexOf('\\n');\ 1426 + var ls=lastNl===-1?0:lastNl+1;\ 1427 + t.setSelectionRange(Math.min(ls+{col},t.value.length),Math.min(ls+{col},t.value.length));") 1428 + } else { 1429 + format!("var firstNl=t.value.indexOf('\\n');\ 1430 + var le=firstNl===-1?t.value.length:firstNl;\ 1431 + t.setSelectionRange(Math.min({col},le),Math.min({col},le));") 1432 + } 1433 + } else { 1434 + // Default: put cursor at end (existing behaviour for mouse clicks / initial mount). 1435 + "t.setSelectionRange(t.value.length,t.value.length);".to_string() 1436 + }; 1437 + drop(nav); // release borrow before mutating 1438 + nav_cursor.set(None); 1412 1439 spawn(async move { 1413 1440 document::eval(&format!( 1414 1441 "var t=document.querySelector('.card-row.active textarea');\ 1415 - if(t){{t.value=atob('{}');t.focus();\ 1416 - t.style.height='auto';t.style.height=(t.scrollHeight+3)+'px';}}", b64 1442 + if(t){{t.value=atob('{b64}');t.focus();{cursor_js}\ 1443 + t.style.height='auto';t.style.height=(t.scrollHeight+3)+'px';}}" 1417 1444 )).await.ok(); 1418 1445 }); 1419 1446 }