a very good jj gui
0
fork

Configure Feed

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

feat: add clickable revision selection with expandable detail view

- Click revision rows to expand/collapse inline detail panel
- Detail panel shows change ID, commit ID, description, author, timestamp
- Placeholder for file changes (TAT-17)
- Removed fixed column widths - description now properly ellipsifies
- Created issues for backend work: TAT-17 (file changes), TAT-18 (timestamp fix)

+66 -27
+13 -2
src/app.rs
··· 36 36 } 37 37 } 38 38 39 + impl Tatami { 40 + pub fn select_revision(&mut self, index: usize, cx: &mut Context<Self>) { 41 + if self.selected_revision == Some(index) { 42 + self.selected_revision = None; 43 + } else { 44 + self.selected_revision = Some(index); 45 + } 46 + cx.notify(); 47 + } 48 + } 49 + 39 50 impl Render for Tatami { 40 - fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { 51 + fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { 41 52 let content = match &self.repo { 42 53 RepoState::NotFound { path } => div() 43 54 .flex_1() ··· 45 56 .child(format!("No jj repository at {}", path.display())), 46 57 RepoState::Loaded { revisions, .. } => div() 47 58 .flex_1() 48 - .child(render_log_view(revisions, self.selected_revision)), 59 + .child(render_log_view(revisions, self.selected_revision, cx)), 49 60 RepoState::Error { message } => { 50 61 div().flex_1().p_3().child(format!("Error: {}", message)) 51 62 }
+53 -25
src/ui/log_view.rs
··· 1 1 use gpui::{ 2 - div, px, rgb, prelude::FluentBuilder, Hsla, InteractiveElement, IntoElement, ParentElement, 3 - SharedString, Styled, 2 + div, px, rgb, prelude::FluentBuilder, Context, Hsla, InteractiveElement, IntoElement, 3 + ParentElement, SharedString, StatefulInteractiveElement, Styled, 4 4 }; 5 5 6 6 use super::theme::{Colors, TextSize}; 7 + use crate::app::Tatami; 7 8 use crate::repo::log::Revision; 8 9 9 10 pub fn render_log_view( 10 11 revisions: &[Revision], 11 12 selected_index: Option<usize>, 13 + cx: &mut Context<Tatami>, 12 14 ) -> impl IntoElement { 15 + let revision_count = revisions.len(); 16 + let entries: Vec<_> = revisions 17 + .iter() 18 + .enumerate() 19 + .map(|(idx, rev)| { 20 + let is_selected = Some(idx) == selected_index; 21 + let is_last = idx == revision_count - 1; 22 + let on_click = cx.listener(move |tatami, _event, _window, cx| { 23 + tatami.select_revision(idx, cx); 24 + }); 25 + (rev.clone(), is_selected, is_last, on_click) 26 + }) 27 + .collect(); 28 + 13 29 div() 14 30 .flex() 15 31 .flex_col() 16 32 .flex_1() 17 33 .overflow_hidden() 18 34 .text_size(TextSize::SM) 19 - .children(revisions.iter().enumerate().map(|(idx, rev)| { 20 - let is_selected = Some(idx) == selected_index; 21 - let is_last = idx == revisions.len() - 1; 22 - render_revision_entry(rev, is_selected, is_last) 23 - })) 35 + .children( 36 + entries 37 + .into_iter() 38 + .map(|(rev, is_selected, is_last, on_click)| { 39 + render_revision_entry(rev, is_selected, is_last, on_click) 40 + }), 41 + ) 24 42 } 25 43 26 - fn render_revision_entry(rev: &Revision, is_selected: bool, is_last: bool) -> impl IntoElement { 44 + fn render_revision_entry<F>( 45 + rev: Revision, 46 + is_selected: bool, 47 + is_last: bool, 48 + on_click: F, 49 + ) -> impl IntoElement 50 + where 51 + F: Fn(&gpui::ClickEvent, &mut gpui::Window, &mut gpui::App) + 'static, 52 + { 27 53 let id_color = if rev.is_working_copy { 28 54 rgb(Colors::WORKING_COPY) 29 55 } else if rev.is_immutable { ··· 46 72 .flex() 47 73 .flex_col() 48 74 .child( 49 - // Main row 50 75 div() 51 76 .id(row_id) 52 77 .w_full() 53 78 .h(px(24.0)) 54 79 .flex() 55 80 .items_center() 81 + .cursor_pointer() 56 82 .hover(|s| s.bg(rgb(Colors::BG_HOVER))) 83 + .on_click(on_click) 57 84 .child(render_graph_column(graph_symbol, id_color.into(), is_last)) 58 85 .child( 59 86 div() 60 87 .flex_1() 61 88 .flex() 62 89 .items_center() 63 - .gap_3() 90 + .gap_2() 91 + .min_w_0() 64 92 .pr_2() 65 93 .child( 66 94 div() 67 95 .flex_shrink_0() 68 - .w(px(96.0)) 69 96 .text_color(id_color) 70 - .overflow_hidden() 71 - .whitespace_nowrap() 72 97 .child(rev.change_id.clone()), 73 98 ) 74 99 .child( ··· 89 114 .child( 90 115 div() 91 116 .flex_shrink_0() 92 - .w(px(100.0)) 93 117 .text_color(rgb(Colors::TEXT_SUBTLE)) 94 - .overflow_hidden() 95 - .whitespace_nowrap() 96 - .text_ellipsis() 97 118 .child(rev.author.clone()), 98 119 ) 99 120 .child( 100 121 div() 101 122 .flex_shrink_0() 102 - .w(px(88.0)) 103 123 .text_color(rgb(Colors::TEXT_SUBTLE)) 104 - .whitespace_nowrap() 105 124 .child(rev.timestamp.clone()), 106 125 ), 107 126 ), 108 127 ) 109 128 .when(is_selected, |el| { 110 - el.child(render_expanded_detail(rev, is_last)) 129 + el.child(render_expanded_detail(&rev, is_last)) 111 130 }) 112 131 } 113 132 ··· 142 161 ) 143 162 } 144 163 145 - fn render_expanded_detail(rev: &Revision, is_last: bool) -> impl IntoElement { 164 + fn render_expanded_detail(rev: &Revision, is_last: bool) -> impl IntoElement + use<> { 146 165 div() 147 166 .flex() 148 167 .child( 149 - // Graph continuation line 150 168 div() 151 169 .flex_shrink_0() 152 170 .w(px(24.0)) ··· 160 178 ), 161 179 ) 162 180 .child( 163 - // Detail panel 164 181 div() 165 182 .flex_1() 166 183 .my_1() ··· 177 194 div() 178 195 .flex() 179 196 .gap_2() 197 + .items_baseline() 180 198 .child( 181 199 div() 182 200 .text_color(rgb(Colors::WORKING_COPY)) 183 - .child(format!("@ {}", rev.change_id)), 201 + .child(rev.change_id.clone()), 184 202 ) 185 203 .child( 186 204 div() 187 205 .text_color(rgb(Colors::TEXT_SUBTLE)) 188 206 .text_size(TextSize::XS) 189 - .child(rev.commit_id.chars().take(12).collect::<String>()), 207 + .child(rev.commit_id.clone()), 190 208 ), 191 209 ) 192 210 .child( ··· 203 221 .text_size(TextSize::XS) 204 222 .text_color(rgb(Colors::TEXT_SUBTLE)) 205 223 .child(format!("{} · {}", rev.author, rev.timestamp)), 224 + ) 225 + .child( 226 + div() 227 + .mt_2() 228 + .pt_2() 229 + .border_t_1() 230 + .border_color(rgb(Colors::BORDER_MUTED)) 231 + .text_size(TextSize::XS) 232 + .text_color(rgb(Colors::TEXT_MUTED)) 233 + .child("(file changes not yet loaded)"), 206 234 ), 207 235 ) 208 236 }