a very good jj gui
0
fork

Configure Feed

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

refactor: simplify UI with theme module and improved log view

+241 -146
+193 -57
src/ui/log_view.rs
··· 1 - use gpui::{div, px, rgb, InteractiveElement, IntoElement, ParentElement, Styled}; 1 + use gpui::{ 2 + div, px, rgb, prelude::FluentBuilder, Hsla, InteractiveElement, IntoElement, ParentElement, 3 + SharedString, Styled, 4 + }; 2 5 6 + use super::theme::{Colors, TextSize}; 3 7 use crate::repo::log::Revision; 4 8 5 - pub fn render_log_view(revisions: &[Revision], selected_index: Option<usize>) -> impl IntoElement { 9 + pub fn render_log_view( 10 + revisions: &[Revision], 11 + selected_index: Option<usize>, 12 + ) -> impl IntoElement { 6 13 div() 7 14 .flex() 8 15 .flex_col() 9 - .size_full() 16 + .flex_1() 10 17 .overflow_hidden() 11 - .children( 12 - revisions 13 - .iter() 14 - .enumerate() 15 - .map(|(idx, rev)| render_revision_row(rev, Some(idx) == selected_index)), 16 - ) 18 + .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 + })) 17 24 } 18 25 19 - fn render_revision_row(rev: &Revision, is_selected: bool) -> impl IntoElement { 20 - let bg_color = if is_selected { 21 - rgb(0x094771) 22 - } else { 23 - rgb(0x1e1e1e) 24 - }; 25 - 26 - let commit_color = if rev.is_working_copy { 27 - rgb(0x4ec9b0) 26 + fn render_revision_entry(rev: &Revision, is_selected: bool, is_last: bool) -> impl IntoElement { 27 + let id_color = if rev.is_working_copy { 28 + rgb(Colors::WORKING_COPY) 28 29 } else if rev.is_immutable { 29 - rgb(0x808080) 30 + rgb(Colors::IMMUTABLE) 30 31 } else { 31 - rgb(0xdcdcaa) 32 + rgb(Colors::MUTABLE) 32 33 }; 33 34 34 35 let graph_symbol = if rev.is_working_copy { ··· 39 40 "○" 40 41 }; 41 42 43 + let row_id: SharedString = format!("rev-{}", rev.change_id).into(); 44 + 42 45 div() 43 - .w_full() 44 - .px_2() 45 - .py_1() 46 - .bg(bg_color) 47 - .hover(|s| s.bg(rgb(0x2a2d2e))) 48 46 .flex() 49 - .gap_2() 47 + .flex_col() 50 48 .child( 49 + // Main row 51 50 div() 52 - .w(px(16.0)) 53 - .text_color(commit_color) 54 - .child(graph_symbol), 51 + .id(row_id) 52 + .w_full() 53 + .h(px(24.0)) 54 + .flex() 55 + .items_center() 56 + .hover(|s| s.bg(rgb(Colors::BG_HOVER))) 57 + .child(render_graph_column(graph_symbol, id_color.into(), is_last)) 58 + .child( 59 + div() 60 + .flex_1() 61 + .flex() 62 + .items_center() 63 + .gap_3() 64 + .pr_2() 65 + .child( 66 + div() 67 + .flex_shrink_0() 68 + .w(px(96.0)) 69 + .text_color(id_color) 70 + .overflow_hidden() 71 + .whitespace_nowrap() 72 + .child(rev.change_id.clone()), 73 + ) 74 + .child( 75 + div() 76 + .flex_1() 77 + .min_w_0() 78 + .text_color(rgb(Colors::TEXT)) 79 + .overflow_hidden() 80 + .whitespace_nowrap() 81 + .text_ellipsis() 82 + .child(if rev.description.is_empty() { 83 + "(no description)".to_string() 84 + } else { 85 + rev.description.clone() 86 + }), 87 + ) 88 + .child(render_bookmarks(&rev.bookmarks)) 89 + .child( 90 + div() 91 + .flex_shrink_0() 92 + .w(px(100.0)) 93 + .text_color(rgb(Colors::TEXT_SUBTLE)) 94 + .overflow_hidden() 95 + .whitespace_nowrap() 96 + .text_ellipsis() 97 + .child(rev.author.clone()), 98 + ) 99 + .child( 100 + div() 101 + .flex_shrink_0() 102 + .w(px(88.0)) 103 + .text_color(rgb(Colors::TEXT_SUBTLE)) 104 + .whitespace_nowrap() 105 + .child(rev.timestamp.clone()), 106 + ), 107 + ), 55 108 ) 109 + .when(is_selected, |el| { 110 + el.child(render_expanded_detail(rev, is_last)) 111 + }) 112 + } 113 + 114 + fn render_graph_column(symbol: &'static str, color: Hsla, is_last: bool) -> impl IntoElement { 115 + div() 116 + .flex_shrink_0() 117 + .w(px(24.0)) 118 + .h(px(24.0)) 119 + .flex() 120 + .flex_col() 121 + .items_center() 56 122 .child( 57 123 div() 58 - .w(px(100.0)) 59 - .text_color(commit_color) 60 - .child(rev.change_id.clone()), 124 + .h(px(4.0)) 125 + .w(px(1.0)) 126 + .bg(rgb(Colors::BORDER_MUTED)), 61 127 ) 62 128 .child( 63 129 div() 64 - .flex_1() 65 - .text_color(rgb(0xcccccc)) 66 - .child(rev.description.clone()), 130 + .h(px(16.0)) 131 + .flex() 132 + .items_center() 133 + .justify_center() 134 + .text_color(color) 135 + .child(symbol), 67 136 ) 68 - .child(render_bookmarks(&rev.bookmarks)) 69 137 .child( 70 138 div() 71 - .w(px(80.0)) 72 - .text_color(rgb(0x808080)) 73 - .child(rev.author.clone()), 139 + .h(px(4.0)) 140 + .w(px(1.0)) 141 + .when(!is_last, |el| el.bg(rgb(Colors::BORDER_MUTED))), 142 + ) 143 + } 144 + 145 + fn render_expanded_detail(rev: &Revision, is_last: bool) -> impl IntoElement { 146 + div() 147 + .flex() 148 + .child( 149 + // Graph continuation line 150 + div() 151 + .flex_shrink_0() 152 + .w(px(24.0)) 153 + .flex() 154 + .justify_center() 155 + .child( 156 + div() 157 + .w(px(1.0)) 158 + .h_full() 159 + .when(!is_last, |el| el.bg(rgb(Colors::BORDER_MUTED))), 160 + ), 74 161 ) 75 162 .child( 163 + // Detail panel 76 164 div() 77 - .w(px(100.0)) 78 - .text_color(rgb(0x808080)) 79 - .child(rev.timestamp.clone()), 165 + .flex_1() 166 + .my_1() 167 + .mr_2() 168 + .p_3() 169 + .bg(rgb(Colors::BG_SURFACE)) 170 + .rounded_md() 171 + .border_1() 172 + .border_color(rgb(Colors::BORDER_MUTED)) 173 + .flex() 174 + .flex_col() 175 + .gap_2() 176 + .child( 177 + div() 178 + .flex() 179 + .gap_2() 180 + .child( 181 + div() 182 + .text_color(rgb(Colors::WORKING_COPY)) 183 + .child(format!("@ {}", rev.change_id)), 184 + ) 185 + .child( 186 + div() 187 + .text_color(rgb(Colors::TEXT_SUBTLE)) 188 + .text_size(TextSize::XS) 189 + .child(rev.commit_id.chars().take(12).collect::<String>()), 190 + ), 191 + ) 192 + .child( 193 + div() 194 + .text_color(rgb(Colors::TEXT)) 195 + .child(if rev.description.is_empty() { 196 + "(no description)".to_string() 197 + } else { 198 + rev.description.clone() 199 + }), 200 + ) 201 + .child( 202 + div() 203 + .text_size(TextSize::XS) 204 + .text_color(rgb(Colors::TEXT_SUBTLE)) 205 + .child(format!("{} · {}", rev.author, rev.timestamp)), 206 + ), 80 207 ) 81 208 } 82 209 83 210 fn render_bookmarks(bookmarks: &[String]) -> impl IntoElement { 84 - div().w(px(120.0)).flex().gap_1().children( 85 - bookmarks 86 - .iter() 87 - .take(2) 88 - .map(|b| { 89 - div() 90 - .px_1() 91 - .rounded_sm() 92 - .bg(rgb(0x4d4d4d)) 93 - .text_color(rgb(0xe0e0e0)) 94 - .text_xs() 95 - .child(b.clone()) 96 - }) 97 - .collect::<Vec<_>>(), 98 - ) 211 + div() 212 + .flex_shrink_0() 213 + .w(px(80.0)) 214 + .flex() 215 + .gap_1() 216 + .overflow_hidden() 217 + .children( 218 + bookmarks 219 + .iter() 220 + .take(2) 221 + .map(|b| { 222 + div() 223 + .flex_shrink_0() 224 + .px(px(6.0)) 225 + .py(px(1.0)) 226 + .rounded_sm() 227 + .bg(rgb(Colors::BG_ELEVATED)) 228 + .text_color(rgb(Colors::ACCENT)) 229 + .text_size(TextSize::XS) 230 + .whitespace_nowrap() 231 + .child(b.clone()) 232 + }) 233 + .collect::<Vec<_>>(), 234 + ) 99 235 }
+1 -1
src/ui/mod.rs
··· 1 1 pub mod log_view; 2 - pub mod status_view; 2 + pub mod theme;
-88
src/ui/status_view.rs
··· 1 - use gpui::{div, px, rgb, IntoElement, ParentElement, Styled}; 2 - 3 - use crate::repo::status::{ChangedFile, FileStatus, WorkingCopyStatus}; 4 - 5 - pub fn render_status_view(status: &WorkingCopyStatus) -> impl IntoElement { 6 - div() 7 - .flex() 8 - .flex_col() 9 - .size_full() 10 - .p_2() 11 - .gap_2() 12 - .child(render_working_copy_info(status)) 13 - .child(render_file_list(&status.files)) 14 - } 15 - 16 - fn render_working_copy_info(status: &WorkingCopyStatus) -> impl IntoElement { 17 - div() 18 - .flex() 19 - .flex_col() 20 - .gap_1() 21 - .pb_2() 22 - .border_b_1() 23 - .border_color(rgb(0x3d3d3d)) 24 - .child( 25 - div() 26 - .flex() 27 - .gap_2() 28 - .child( 29 - div() 30 - .text_color(rgb(0x4ec9b0)) 31 - .child(format!("@ {}", status.change_id)), 32 - ) 33 - .child( 34 - div() 35 - .text_color(rgb(0x808080)) 36 - .child(status.commit_id.clone()), 37 - ), 38 - ) 39 - .child( 40 - div() 41 - .text_color(rgb(0xcccccc)) 42 - .child(if status.description.is_empty() { 43 - "(no description)".to_string() 44 - } else { 45 - status.description.clone() 46 - }), 47 - ) 48 - .child( 49 - div() 50 - .text_color(rgb(0x606060)) 51 - .text_sm() 52 - .child(format!("Parent: {}", status.parent_description)), 53 - ) 54 - } 55 - 56 - fn render_file_list(files: &[ChangedFile]) -> impl IntoElement { 57 - div() 58 - .flex() 59 - .flex_col() 60 - .gap_1() 61 - .child( 62 - div() 63 - .text_color(rgb(0x808080)) 64 - .text_sm() 65 - .child(format!("Changes ({} files)", files.len())), 66 - ) 67 - .children(files.iter().map(render_file_row).collect::<Vec<_>>()) 68 - } 69 - 70 - fn render_file_row(file: &ChangedFile) -> impl IntoElement { 71 - let (status_char, status_color) = match file.status { 72 - FileStatus::Added => ("A", rgb(0x4ec9b0)), 73 - FileStatus::Modified => ("M", rgb(0xdcdcaa)), 74 - FileStatus::Deleted => ("D", rgb(0xf14c4c)), 75 - }; 76 - 77 - div() 78 - .flex() 79 - .gap_2() 80 - .py(px(2.0)) 81 - .child( 82 - div() 83 - .w(px(16.0)) 84 - .text_color(status_color) 85 - .child(status_char), 86 - ) 87 - .child(div().text_color(rgb(0xcccccc)).child(file.path.clone())) 88 - }
+47
src/ui/theme.rs
··· 1 + use gpui::{px, Pixels, SharedString}; 2 + 3 + const FONT_MONO: &str = "Berkeley Mono"; 4 + 5 + pub fn font_family() -> SharedString { 6 + SharedString::from(FONT_MONO) 7 + } 8 + 9 + pub struct TextSize; 10 + 11 + impl TextSize { 12 + pub const XS: Pixels = px(10.0); 13 + pub const SM: Pixels = px(12.0); 14 + pub const BASE: Pixels = px(13.0); 15 + } 16 + 17 + pub struct Colors; 18 + 19 + impl Colors { 20 + // Backgrounds 21 + pub const BG_BASE: u32 = 0x0d1117; 22 + pub const BG_SURFACE: u32 = 0x161b22; 23 + pub const BG_ELEVATED: u32 = 0x21262d; 24 + pub const BG_HOVER: u32 = 0x30363d; 25 + pub const BG_SELECTED: u32 = 0x1f6feb; 26 + 27 + // Borders 28 + pub const BORDER_MUTED: u32 = 0x21262d; 29 + 30 + // Text 31 + pub const TEXT: u32 = 0xe6edf3; 32 + pub const TEXT_MUTED: u32 = 0x8b949e; 33 + pub const TEXT_SUBTLE: u32 = 0x6e7681; 34 + 35 + // Semantic 36 + pub const ACCENT: u32 = 0x58a6ff; 37 + 38 + // Git status 39 + pub const ADDED: u32 = 0x3fb950; 40 + pub const MODIFIED: u32 = 0xd29922; 41 + pub const DELETED: u32 = 0xf85149; 42 + 43 + // Revision colors 44 + pub const WORKING_COPY: u32 = 0x58a6ff; 45 + pub const MUTABLE: u32 = 0xe6edf3; 46 + pub const IMMUTABLE: u32 = 0x6e7681; 47 + }