@@ -30,7 +30,7 @@ use url::Url;
3030const BACKLINK_PAGE_SIZE : usize = 12 ;
3131const HEAVY_RECOMPUTE_IDLE_DEBOUNCE : Duration = Duration :: from_millis ( 250 ) ;
3232
33- #[ derive( Clone , Copy , PartialEq , Eq ) ]
33+ #[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
3434enum BacklinkTab {
3535 LinkedTodos ,
3636 RelatedNotes ,
@@ -161,6 +161,7 @@ pub struct NotePanel {
161161 image_cache : HashMap < std:: path:: PathBuf , egui:: TextureHandle > ,
162162 overwrite_prompt : bool ,
163163 show_open_with_menu : bool ,
164+ show_metadata : bool ,
164165 tags_expanded : bool ,
165166 links_expanded : bool ,
166167 backlink_tab : BacklinkTab ,
@@ -183,6 +184,17 @@ pub struct NotePanel {
183184 last_todo_revision : u64 ,
184185 #[ cfg( test) ]
185186 heavy_recompute_count : usize ,
187+ #[ cfg( test) ]
188+ last_ui_sections : NotePanelUiSections ,
189+ }
190+
191+ #[ cfg( test) ]
192+ #[ derive( Default , Clone , Copy ) ]
193+ struct NotePanelUiSections {
194+ tags_visible : bool ,
195+ links_visible : bool ,
196+ backlinks_visible : bool ,
197+ content_visible : bool ,
186198}
187199
188200#[ derive( Default , Clone ) ]
@@ -197,6 +209,14 @@ struct NoteDerivedView {
197209}
198210
199211impl NotePanel {
212+ fn details_toggle_label ( & self ) -> & ' static str {
213+ if self . show_metadata {
214+ "Hide Details"
215+ } else {
216+ "Show Details"
217+ }
218+ }
219+
200220 pub fn from_note ( note : Note ) -> Self {
201221 let mut panel = Self {
202222 open : true ,
@@ -209,6 +229,7 @@ impl NotePanel {
209229 image_cache : HashMap :: new ( ) ,
210230 overwrite_prompt : false ,
211231 show_open_with_menu : false ,
232+ show_metadata : true ,
212233 tags_expanded : false ,
213234 links_expanded : false ,
214235 backlink_tab : BacklinkTab :: LinkedTodos ,
@@ -227,6 +248,8 @@ impl NotePanel {
227248 last_todo_revision : 0 ,
228249 #[ cfg( test) ]
229250 heavy_recompute_count : 0 ,
251+ #[ cfg( test) ]
252+ last_ui_sections : NotePanelUiSections :: default ( ) ,
230253 } ;
231254 panel. refresh_fast_derived ( ) ;
232255 panel. refresh_heavy_derived ( true ) ;
@@ -340,6 +363,10 @@ impl NotePanel {
340363 . max_height ( max_height)
341364 . movable ( true )
342365 . show ( ctx, |ui| {
366+ #[ cfg( test) ]
367+ {
368+ self . last_ui_sections = NotePanelUiSections :: default ( ) ;
369+ }
343370 if ui
344371 . ctx ( )
345372 . input ( |i| i. modifiers . ctrl && i. key_pressed ( Key :: Equals ) )
@@ -420,6 +447,16 @@ impl NotePanel {
420447 ui. ctx ( ) . memory_mut ( |m| m. surrender_focus ( id) ) ;
421448 }
422449 }
450+ if ui. button ( self . details_toggle_label ( ) ) . clicked ( ) {
451+ self . show_metadata = !self . show_metadata ;
452+ let was_focused = self
453+ . last_textedit_id
454+ . map ( |id| ui. ctx ( ) . memory ( |m| m. has_focus ( id) ) )
455+ . unwrap_or ( false ) ;
456+ if was_focused {
457+ self . focus_textedit_next_frame = true ;
458+ }
459+ }
423460 ui. separator ( ) ;
424461 if ui. button ( "A-" ) . clicked ( ) {
425462 app. note_font_size = ( app. note_font_size - 1.0 ) . max ( 8.0 ) ;
@@ -432,7 +469,11 @@ impl NotePanel {
432469 self . refresh_fast_derived ( ) ;
433470 }
434471 self . maybe_refresh_heavy_derived ( ctx) ;
435- if !self . derived . tags . is_empty ( ) {
472+ if self . show_metadata && !self . derived . tags . is_empty ( ) {
473+ #[ cfg( test) ]
474+ {
475+ self . last_ui_sections . tags_visible = true ;
476+ }
436477 let was_focused = self
437478 . last_textedit_id
438479 . map ( |id| ui. ctx ( ) . memory ( |m| m. has_focus ( id) ) )
@@ -477,7 +518,11 @@ impl NotePanel {
477518 . into_iter ( )
478519 . map ( |( label, url) | LinkKind :: Url ( label, url) ) ,
479520 ) ;
480- if !all_links. is_empty ( ) {
521+ if self . show_metadata && !all_links. is_empty ( ) {
522+ #[ cfg( test) ]
523+ {
524+ self . last_ui_sections . links_visible = true ;
525+ }
481526 let was_focused = self
482527 . last_textedit_id
483528 . map ( |id| ui. ctx ( ) . memory ( |m| m. has_focus ( id) ) )
@@ -513,74 +558,91 @@ impl NotePanel {
513558 }
514559 } ) ;
515560 }
516- ui. separator ( ) ;
517- ui. label ( "Backlinks" ) ;
518- ui. horizontal ( |ui| {
519- for tab in [
520- BacklinkTab :: LinkedTodos ,
521- BacklinkTab :: RelatedNotes ,
522- BacklinkTab :: Mentions ,
523- ] {
524- if ui
525- . selectable_label ( self . backlink_tab == tab, tab. label ( ) )
526- . clicked ( )
527- {
528- self . backlink_tab = tab;
529- self . backlink_page = 0 ;
530- }
561+ if self . show_metadata {
562+ #[ cfg( test) ]
563+ {
564+ self . last_ui_sections . backlinks_visible = true ;
531565 }
532- } ) ;
533- let rows = self . backlink_rows_for_active_tab ( ) ;
534- let total_pages = ( rows. len ( ) + BACKLINK_PAGE_SIZE - 1 ) / BACKLINK_PAGE_SIZE ;
535- let page_start = self . backlink_page * BACKLINK_PAGE_SIZE ;
536- let page_end = ( page_start + BACKLINK_PAGE_SIZE ) . min ( rows. len ( ) ) ;
537- if rows. is_empty ( ) {
538- ui. small ( "No backlinks in this category." ) ;
539- } else {
540- for ( idx, row) in rows[ page_start..page_end] . iter ( ) . enumerate ( ) {
541- ui. push_id ( ( "backlink_row" , idx, page_start) , |ui| {
542- let resp = ui. selectable_label ( false , & row. title ) ;
543- if resp. clicked ( ) {
544- if let Some ( slug) = & row. note_slug {
545- app. open_note_panel ( slug, None ) ;
546- } else if let Some ( todo_id) = & row. todo_id {
547- let todos = load_todos ( TODO_FILE ) . unwrap_or_default ( ) ;
548- if let Some ( ( todo_idx, _) ) =
549- todos. iter ( ) . enumerate ( ) . find ( |( _, t) | & t. id == todo_id)
550- {
551- app. todo_view_dialog . open_edit ( todo_idx) ;
552- } else {
553- app. todo_view_dialog . open ( ) ;
566+ ui. separator ( ) ;
567+ ui. label ( "Backlinks" ) ;
568+ ui. horizontal ( |ui| {
569+ for tab in [
570+ BacklinkTab :: LinkedTodos ,
571+ BacklinkTab :: RelatedNotes ,
572+ BacklinkTab :: Mentions ,
573+ ] {
574+ if ui
575+ . selectable_label ( self . backlink_tab == tab, tab. label ( ) )
576+ . clicked ( )
577+ {
578+ self . backlink_tab = tab;
579+ self . backlink_page = 0 ;
580+ }
581+ }
582+ } ) ;
583+ let rows = self . backlink_rows_for_active_tab ( ) ;
584+ let total_pages = ( rows. len ( ) + BACKLINK_PAGE_SIZE - 1 ) / BACKLINK_PAGE_SIZE ;
585+ let page_start = self . backlink_page * BACKLINK_PAGE_SIZE ;
586+ let page_end = ( page_start + BACKLINK_PAGE_SIZE ) . min ( rows. len ( ) ) ;
587+ if rows. is_empty ( ) {
588+ ui. small ( "No backlinks in this category." ) ;
589+ } else {
590+ for ( idx, row) in rows[ page_start..page_end] . iter ( ) . enumerate ( ) {
591+ ui. push_id ( ( "backlink_row" , idx, page_start) , |ui| {
592+ let resp = ui. selectable_label ( false , & row. title ) ;
593+ if resp. clicked ( ) {
594+ if let Some ( slug) = & row. note_slug {
595+ app. open_note_panel ( slug, None ) ;
596+ } else if let Some ( todo_id) = & row. todo_id {
597+ let todos = load_todos ( TODO_FILE ) . unwrap_or_default ( ) ;
598+ if let Some ( ( todo_idx, _) ) =
599+ todos. iter ( ) . enumerate ( ) . find ( |( _, t) | & t. id == todo_id)
600+ {
601+ app. todo_view_dialog . open_edit ( todo_idx) ;
602+ } else {
603+ app. todo_view_dialog . open ( ) ;
604+ }
554605 }
555606 }
556- }
557- if resp. has_focus ( ) && ui. input ( |i| i. key_pressed ( egui:: Key :: Enter ) ) {
558- if let Some ( slug) = & row. note_slug {
559- app. open_note_panel ( slug, None ) ;
607+ if resp. has_focus ( ) && ui. input ( |i| i. key_pressed ( egui:: Key :: Enter ) )
608+ {
609+ if let Some ( slug) = & row. note_slug {
610+ app. open_note_panel ( slug, None ) ;
611+ }
560612 }
561- }
562- ui. horizontal_wrapped ( |ui| {
563- ui. small ( format ! ( "[{}]" , row. type_badge) ) ;
564- ui. small ( format ! ( "updated {}" , row. updated) ) ;
613+ ui. horizontal_wrapped ( |ui| {
614+ ui. small ( format ! ( "[{}]" , row. type_badge) ) ;
615+ ui. small ( format ! ( "updated {}" , row. updated) ) ;
616+ } ) ;
617+ ui. small ( & row. snippet ) ;
565618 } ) ;
566- ui. small ( & row. snippet ) ;
567- } ) ;
568- ui. separator ( ) ;
569- }
570- if total_pages > 1 {
571- ui. horizontal ( |ui| {
572- if ui. button ( "Prev" ) . clicked ( ) && self . backlink_page > 0 {
573- self . backlink_page -= 1 ;
574- }
575- ui. small ( format ! ( "Page {}/{}" , self . backlink_page + 1 , total_pages) ) ;
576- if ui. button ( "Next" ) . clicked ( ) && self . backlink_page + 1 < total_pages {
577- self . backlink_page += 1 ;
578- }
579- } ) ;
619+ ui. separator ( ) ;
620+ }
621+ if total_pages > 1 {
622+ ui. horizontal ( |ui| {
623+ if ui. button ( "Prev" ) . clicked ( ) && self . backlink_page > 0 {
624+ self . backlink_page -= 1 ;
625+ }
626+ ui. small ( format ! (
627+ "Page {}/{}" ,
628+ self . backlink_page + 1 ,
629+ total_pages
630+ ) ) ;
631+ if ui. button ( "Next" ) . clicked ( )
632+ && self . backlink_page + 1 < total_pages
633+ {
634+ self . backlink_page += 1 ;
635+ }
636+ } ) ;
637+ }
580638 }
639+ ui. separator ( ) ;
581640 }
582- ui. separator ( ) ;
583641 let remaining = ui. available_height ( ) ;
642+ #[ cfg( test) ]
643+ {
644+ self . last_ui_sections . content_visible = true ;
645+ }
584646 let resp = egui:: ScrollArea :: vertical ( )
585647 . id_source ( scroll_id_source)
586648 . max_height ( remaining)
@@ -1774,6 +1836,12 @@ mod tests {
17741836 }
17751837 }
17761838
1839+ fn render_panel_once ( ctx : & egui:: Context , panel : & mut NotePanel , app : & mut LauncherApp ) {
1840+ let _ = ctx. run ( Default :: default ( ) , |ctx| {
1841+ panel. ui ( ctx, app) ;
1842+ } ) ;
1843+ }
1844+
17771845 #[ test]
17781846 fn wrap_selection_preserves_range ( ) {
17791847 let ctx = egui:: Context :: default ( ) ;
@@ -2068,6 +2136,50 @@ mod tests {
20682136 assert ! ( mentions. len( ) >= 1 ) ;
20692137 }
20702138
2139+ #[ test]
2140+ fn toggle_hides_metadata_sections_in_ui ( ) {
2141+ let ctx = egui:: Context :: default ( ) ;
2142+ let mut app = new_app ( & ctx) ;
2143+ let mut panel = NotePanel :: from_note ( empty_note (
2144+ "#tag [[linked-note]] https://example.com\n \n Body visible always" ,
2145+ ) ) ;
2146+ panel. preview_mode = false ;
2147+
2148+ render_panel_once ( & ctx, & mut panel, & mut app) ;
2149+ assert ! ( panel. last_ui_sections. tags_visible) ;
2150+ assert ! ( panel. last_ui_sections. links_visible) ;
2151+ assert ! ( panel. last_ui_sections. backlinks_visible) ;
2152+ assert ! ( panel. last_ui_sections. content_visible) ;
2153+
2154+ panel. show_metadata = false ;
2155+ render_panel_once ( & ctx, & mut panel, & mut app) ;
2156+ assert ! ( !panel. last_ui_sections. tags_visible) ;
2157+ assert ! ( !panel. last_ui_sections. links_visible) ;
2158+ assert ! ( !panel. last_ui_sections. backlinks_visible) ;
2159+ assert ! ( panel. last_ui_sections. content_visible) ;
2160+ }
2161+
2162+ #[ test]
2163+ fn toggle_button_label_reflects_state ( ) {
2164+ let mut panel = NotePanel :: from_note ( empty_note ( "body" ) ) ;
2165+ assert_eq ! ( panel. details_toggle_label( ) , "Hide Details" ) ;
2166+ panel. show_metadata = false ;
2167+ assert_eq ! ( panel. details_toggle_label( ) , "Show Details" ) ;
2168+ }
2169+
2170+ #[ test]
2171+ fn toggle_preserves_tab_and_pagination_state ( ) {
2172+ let mut panel = NotePanel :: from_note ( empty_note ( "body" ) ) ;
2173+ panel. backlink_tab = BacklinkTab :: Mentions ;
2174+ panel. backlink_page = 2 ;
2175+
2176+ panel. show_metadata = false ;
2177+ panel. show_metadata = true ;
2178+
2179+ assert_eq ! ( panel. backlink_tab, BacklinkTab :: Mentions ) ;
2180+ assert_eq ! ( panel. backlink_page, 2 ) ;
2181+ }
2182+
20712183 #[ test]
20722184 fn derived_metadata_is_reused_without_save ( ) {
20732185 let ctx = egui:: Context :: default ( ) ;
0 commit comments