Support automatically toggling room focus (#337)

This commit is contained in:
Ulyssa 2025-05-31 09:29:49 -07:00 committed by GitHub
parent 84eaadc09a
commit 9ed9400b67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 203 additions and 17 deletions

View file

@ -1766,6 +1766,13 @@ impl RoomFocus {
pub fn is_msgbar(&self) -> bool {
matches!(self, RoomFocus::MessageBar)
}
pub fn toggle(&mut self) {
*self = match self {
RoomFocus::MessageBar => RoomFocus::Scrollback,
RoomFocus::Scrollback => RoomFocus::MessageBar,
};
}
}
/// Identifiers used to track where a mark was placed.

View file

@ -1141,18 +1141,18 @@ mod tests {
let mut cmds = setup_commands();
let ctx = EditContext::default();
let cmd = format!("room notify set mute");
let res = cmds.input_cmd(&cmd, ctx.clone()).unwrap();
let cmd = "room notify set mute";
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
let act = RoomAction::Set(RoomField::NotificationMode, "mute".into());
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let cmd = format!("room notify unset");
let res = cmds.input_cmd(&cmd, ctx.clone()).unwrap();
let cmd = "room notify unset";
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
let act = RoomAction::Unset(RoomField::NotificationMode);
assert_eq!(res, vec![(act.into(), ctx.clone())]);
let cmd = format!("room notify show");
let res = cmds.input_cmd(&cmd, ctx.clone()).unwrap();
let cmd = "room notify show";
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
let act = RoomAction::Show(RoomField::NotificationMode);
assert_eq!(res, vec![(act.into(), ctx.clone())]);
}

View file

@ -638,10 +638,7 @@ impl ChatState {
}
pub fn focus_toggle(&mut self) {
self.focus = match self.focus {
RoomFocus::Scrollback => RoomFocus::MessageBar,
RoomFocus::MessageBar => RoomFocus::Scrollback,
};
self.focus.toggle();
}
pub fn room(&self) -> &MatrixRoom {
@ -652,6 +649,14 @@ impl ChatState {
&self.room_id
}
pub fn auto_toggle_focus(
&mut self,
act: &EditorAction,
ctx: &ProgramContext,
) -> Option<EditorAction> {
auto_toggle_focus(&mut self.focus, act, ctx, &self.scrollback, &mut self.tbox)
}
pub fn typing_notice(
&self,
act: &EditorAction,
@ -754,8 +759,15 @@ impl Editable<ProgramContext, ProgramStore, IambInfo> for ChatState {
ctx: &ProgramContext,
store: &mut ProgramStore,
) -> EditResult<EditInfo, IambInfo> {
// Check whether we should automatically switch between the message bar
// or message scrollback, and use an adjusted action if we do so.
let adjusted = self.auto_toggle_focus(act, ctx);
let act = adjusted.as_ref().unwrap_or(act);
// Send typing notice if needed.
self.typing_notice(act, ctx, store);
// And now we can finally run the editor command.
match delegate!(self, w => w.editor_command(act, ctx, store)) {
res @ Ok(_) => res,
Err(EditError::WrongBuffer(IambBufferId::Room(room_id, thread, focus)))
@ -993,3 +1005,158 @@ fn cmd(open_command: &Vec<String>) -> Option<Command> {
}
None
}
pub fn auto_toggle_focus(
focus: &mut RoomFocus,
act: &EditorAction,
ctx: &ProgramContext,
scrollback: &ScrollbackState,
tbox: &mut TextBoxState<IambInfo>,
) -> Option<EditorAction> {
let is_insert = ctx.get_insert_style().is_some();
match (focus, act) {
(f @ RoomFocus::Scrollback, _) if is_insert => {
// Insert mode commands should switch focus.
f.toggle();
None
},
(f @ RoomFocus::Scrollback, EditorAction::InsertText(_)) => {
// Pasting or otherwise inserting text should switch.
f.toggle();
None
},
(
f @ RoomFocus::Scrollback,
EditorAction::Edit(
op,
EditTarget::Motion(mov @ MoveType::Line(MoveDir1D::Next), count),
),
) if ctx.resolve(op).is_motion() => {
let count = ctx.resolve(count);
if count > 0 && scrollback.is_latest() {
// Trying to move down a line when already at the end of room history should
// switch.
f.toggle();
// And decrement the count for the action.
let count = count.saturating_sub(1).into();
let target = EditTarget::Motion(mov.clone(), count);
let dec = EditorAction::Edit(op.clone(), target);
Some(dec)
} else {
None
}
},
(
f @ RoomFocus::MessageBar,
EditorAction::Edit(
op,
EditTarget::Motion(mov @ MoveType::Line(MoveDir1D::Previous), count),
),
) if !is_insert && ctx.resolve(op).is_motion() => {
let count = ctx.resolve(count);
if count > 0 && tbox.get_cursor().y == 0 {
// Trying to move up a line when already at the top of the msgbar should
// switch as long as we're not in Insert mode.
f.toggle();
// And decrement the count for the action.
let count = count.saturating_sub(1).into();
let target = EditTarget::Motion(mov.clone(), count);
let dec = EditorAction::Edit(op.clone(), target);
Some(dec)
} else {
None
}
},
(RoomFocus::Scrollback, _) | (RoomFocus::MessageBar, _) => {
// Do not switch.
None
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use modalkit::actions::{EditAction, InsertTextAction};
use crate::tests::{mock_store, TEST_ROOM1_ID};
macro_rules! move_line {
($dir: expr, $count: expr) => {
EditorAction::Edit(
EditAction::Motion.into(),
EditTarget::Motion(MoveType::Line($dir), $count.into()),
)
};
}
#[tokio::test]
async fn test_auto_focus() {
let mut store = mock_store().await;
let ctx = ProgramContext::default();
let room_id = TEST_ROOM1_ID.clone();
let scrollback = ScrollbackState::new(room_id.clone(), None);
let id = IambBufferId::Room(room_id, None, RoomFocus::MessageBar);
let ebuf = store.load_buffer(id);
let mut tbox = TextBoxState::new(ebuf);
// Start out focused on the scrollback.
let mut focused = RoomFocus::Scrollback;
// Inserting text toggles:
let act = EditorAction::InsertText(InsertTextAction::Type(
Char::from('a').into(),
MoveDir1D::Next,
1.into(),
));
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
assert_eq!(focused, RoomFocus::MessageBar);
assert!(res.is_none());
// Going down in message bar doesn't toggle:
let act = move_line!(MoveDir1D::Next, 1);
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
assert_eq!(focused, RoomFocus::MessageBar);
assert!(res.is_none());
// But going up will:
let act = move_line!(MoveDir1D::Previous, 1);
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
assert_eq!(focused, RoomFocus::Scrollback);
assert_eq!(res, Some(move_line!(MoveDir1D::Previous, 0)));
// Going up in scrollback doesn't toggle:
let act = move_line!(MoveDir1D::Previous, 1);
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
assert_eq!(focused, RoomFocus::Scrollback);
assert_eq!(res, None);
// And then go back down:
let act = move_line!(MoveDir1D::Next, 1);
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
assert_eq!(focused, RoomFocus::MessageBar);
assert_eq!(res, Some(move_line!(MoveDir1D::Next, 0)));
// Go up 2 will go up 1 in scrollback:
let act = move_line!(MoveDir1D::Previous, 2);
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
assert_eq!(focused, RoomFocus::Scrollback);
assert_eq!(res, Some(move_line!(MoveDir1D::Previous, 1)));
// Go down 3 will go down 2 in messagebar:
let act = move_line!(MoveDir1D::Next, 3);
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
assert_eq!(focused, RoomFocus::MessageBar);
assert_eq!(res, Some(move_line!(MoveDir1D::Next, 2)));
}
}

View file

@ -79,14 +79,20 @@ fn nth_key_before(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey {
}
fn nth_before(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor {
nth_key_before(pos, n, thread).into()
let key = nth_key_before(pos, n, thread);
if matches!(thread.last_key_value(), Some((last, _)) if &key == last) {
MessageCursor::latest()
} else {
MessageCursor::from(key)
}
}
fn nth_key_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey {
fn nth_key_after(pos: MessageKey, n: usize, thread: &Messages) -> Option<MessageKey> {
let mut end = &pos;
let iter = thread.range(&pos..).enumerate();
let mut iter = thread.range(&pos..).enumerate();
for (i, (key, _)) in iter {
for (i, (key, _)) in iter.by_ref() {
end = key;
if i >= n {
@ -94,11 +100,12 @@ fn nth_key_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey {
}
}
end.clone()
// Avoid returning the key if it's at the end.
iter.next().map(|_| end.clone())
}
fn nth_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor {
nth_key_after(pos, n, thread).into()
nth_key_after(pos, n, thread).map(MessageCursor::from).unwrap_or_default()
}
fn prevmsg<'a>(key: &MessageKey, thread: &'a Messages) -> Option<&'a Message> {
@ -150,6 +157,10 @@ impl ScrollbackState {
}
}
pub fn is_latest(&self) -> bool {
self.cursor.timestamp.is_none()
}
pub fn goto_latest(&mut self) {
self.cursor = MessageCursor::latest();
}
@ -1524,8 +1535,9 @@ mod tests {
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
assert_eq!(scrollback.cursor, MSG5_KEY.clone().into());
// And one more becomes "latest" cursor:
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
assert_eq!(scrollback.cursor, MSG1_KEY.clone().into());
assert_eq!(scrollback.cursor, MessageCursor::latest());
}
#[tokio::test]