@@ -3,12 +3,17 @@ use crate::ui::theme::{self, rd};
33
44/// Draw the right-panel details/preview area.
55pub fn draw_preview ( ui : & mut egui:: Ui , app : & mut CascExplorerApp ) {
6- ui. add_space ( 2.0 ) ;
7- ui. label ( theme:: engraved ( "Details / Preview" ) ) ;
8- ui. separator ( ) ;
6+ let pill = app
7+ . selected
8+ . as_ref ( )
9+ . and_then ( |s| s. result . filename . as_deref ( ) )
10+ . and_then ( |n| n. rsplit ( '.' ) . next ( ) )
11+ . map ( str:: to_uppercase) ;
12+ theme:: panel_header ( ui, "Details / Preview" , pill. as_deref ( ) ) ;
913
1014 if app. selected . is_none ( ) {
11- ui. label ( "Select a file to preview it." ) ;
15+ ui. add_space ( 8.0 ) ;
16+ ui. label ( egui:: RichText :: new ( "Select a file to preview it." ) . color ( rd:: FG_MUTED ) ) ;
1217 return ;
1318 }
1419
@@ -53,18 +58,20 @@ fn draw_preview_body(ui: &mut egui::Ui, app: &mut CascExplorerApp) {
5358 }
5459 } ) ;
5560
56- ui. separator ( ) ;
57-
5861 // ── Error ──────────────────────────────────────────────────────────────────
5962 if let Some ( err) = & sel. load_error {
63+ theme:: section_label ( ui, "Error" ) ;
6064 ui. colored_label ( rd:: DANGER , format ! ( "⚠ {err}" ) ) ;
6165 return ;
6266 }
6367
6468 // ── Loading spinner while the background task is running ──────────────────
6569 if sel. data . is_none ( ) && sel. load_error . is_none ( ) && app. loading {
66- ui. spinner ( ) ;
67- ui. label ( "Loading file data…" ) ;
70+ ui. add_space ( 8.0 ) ;
71+ ui. horizontal ( |ui| {
72+ ui. spinner ( ) ;
73+ ui. label ( egui:: RichText :: new ( "Loading file data…" ) . color ( rd:: FG_MUTED ) ) ;
74+ } ) ;
6875 return ;
6976 }
7077
@@ -81,8 +88,8 @@ fn draw_preview_body(ui: &mut egui::Ui, app: &mut CascExplorerApp) {
8188 None => "Auto" . to_string ( ) ,
8289 Some ( i) => names. get ( i) . cloned ( ) . unwrap_or_else ( || format ! ( "#{i}" ) ) ,
8390 } ;
91+ theme:: section_label ( ui, "Viewer" ) ;
8492 ui. horizontal ( |ui| {
85- ui. label ( "Viewer:" ) ;
8693 egui:: ComboBox :: from_id_salt ( "preview_override" )
8794 . selected_text ( current_label)
8895 . show_ui ( ui, |ui| {
@@ -102,41 +109,48 @@ fn draw_preview_body(ui: &mut egui::Ui, app: &mut CascExplorerApp) {
102109 }
103110 } ) ;
104111 } ) ;
105- ui. separator ( ) ;
106112 }
107113
108114 // ── Plugin-provided preview ───────────────────────────────────────────────
109115 if let Some ( preview) = & sel. preview {
110116 // Texture (inline image).
111117 if let Some ( tex) = & preview. texture {
112118 let tex_size = tex. size_vec2 ( ) ;
119+ theme:: section_label (
120+ ui,
121+ & format ! (
122+ "Texture · {}×{}" ,
123+ tex_size. x. round( ) as u32 ,
124+ tex_size. y. round( ) as u32
125+ ) ,
126+ ) ;
113127 let max_w = ui. available_width ( ) ;
114128 let scale = ( max_w / tex_size. x ) . min ( 1.0 ) ;
115129 let display_size = tex_size * scale;
116130 ui. image ( ( tex. id ( ) , display_size) ) ;
117- ui. separator ( ) ;
118131 }
119132
120133 // 3D mesh viewport (currently only WMO group geometry).
121134 if let Some ( mesh) = preview. mesh3d . clone ( ) {
135+ theme:: section_label ( ui, "Mesh" ) ;
122136 crate :: viewport3d:: paint_mesh ( ui, mesh) ;
123- ui. separator ( ) ;
124137 }
125138
126139 // Text block (formatted summary or full text file). The outer
127140 // ScrollArea handles overflow; this widget just lays out as tall
128141 // as its contents.
129142 if let Some ( text) = & preview. text {
143+ theme:: section_label ( ui, "Text" ) ;
130144 ui. add (
131145 egui:: TextEdit :: multiline ( & mut text. as_str ( ) )
132146 . font ( egui:: TextStyle :: Monospace )
133147 . desired_width ( f32:: INFINITY ) ,
134148 ) ;
135- ui. separator ( ) ;
136149 }
137150 } else if let Some ( data) = & sel. data {
138151 // ── Hex-dump fallback (no plugin claimed this file) ───────────────────
139152 let preview_len = data. len ( ) . min ( 256 ) ;
153+ theme:: section_label ( ui, & format ! ( "Hex · first {preview_len} bytes" ) ) ;
140154 let hex: String = data[ ..preview_len]
141155 . chunks ( 16 )
142156 . enumerate ( )
@@ -166,22 +180,20 @@ fn draw_preview_body(ui: &mut egui::Ui, app: &mut CascExplorerApp) {
166180
167181 // ── Deep-search content matches ────────────────────────────────────────────
168182 if !sel. content_matches . is_empty ( ) {
169- ui. separator ( ) ;
170- ui. label ( format ! (
171- "Deep search: {} entries" ,
172- sel. content_matches. len( )
173- ) ) ;
183+ theme:: section_label (
184+ ui,
185+ & format ! ( "Deep search · {} entries" , sel. content_matches. len( ) ) ,
186+ ) ;
174187 for m in & sel. content_matches {
175188 ui. label (
176189 egui:: RichText :: new ( format ! ( "[{}] {}" , m. kind, m. inner_path) )
177190 . small ( )
178- . monospace ( ) ,
191+ . monospace ( )
192+ . color ( rd:: FG_PRIMARY ) ,
179193 ) ;
180194 }
181195 }
182196
183- ui. separator ( ) ;
184-
185197 // ── PCX palette picker (for SC1 assets with external palettes) ───────────
186198 let is_pcx = sel
187199 . result
@@ -199,6 +211,7 @@ fn draw_preview_body(ui: &mut egui::Ui, app: &mut CascExplorerApp) {
199211 let mut audio_action: Option < crate :: app:: AudioAction > = None ;
200212
201213 if is_pcx {
214+ theme:: section_label ( ui, "PCX palette" ) ;
202215 ui. horizontal ( |ui| {
203216 if ui
204217 . button ( "Load Palette…" )
@@ -218,7 +231,6 @@ fn draw_preview_body(ui: &mut egui::Ui, app: &mut CascExplorerApp) {
218231 ) ;
219232 }
220233 } ) ;
221- ui. separator ( ) ;
222234 }
223235
224236 // ── Audio playback controls ───────────────────────────────────────────────
@@ -234,6 +246,7 @@ fn draw_preview_body(ui: &mut egui::Ui, app: &mut CascExplorerApp) {
234246 . map ( crate :: audio:: is_audio_filename)
235247 . unwrap_or ( false ) ;
236248 if is_audio && sel. data . is_some ( ) {
249+ theme:: section_label ( ui, "Audio" ) ;
237250 // Look at the live player (if any) to decide button states.
238251 // We can't borrow `app` mutably here, but the immutable peek
239252 // at `audio_player` is fine because we only read is_playing /
@@ -320,12 +333,11 @@ fn draw_preview_body(ui: &mut egui::Ui, app: &mut CascExplorerApp) {
320333 ui. ctx ( )
321334 . request_repaint_after ( std:: time:: Duration :: from_millis ( 200 ) ) ;
322335 }
323-
324- ui. separator ( ) ;
325336 }
326337
327338 // ── Export buttons ─────────────────────────────────────────────────────────
328339 if sel. data . is_some ( ) {
340+ theme:: section_label ( ui, "Export" ) ;
329341 ui. horizontal ( |ui| {
330342 // Plugin-provided exports (e.g. "Export As PNG", "Export As BK2").
331343 if let Some ( preview) = & sel. preview {
@@ -548,25 +560,20 @@ fn export_raw(app: &CascExplorerApp) {
548560}
549561
550562fn meta_row ( ui : & mut egui:: Ui , label : & str , value : & str ) {
551- // Muted key, primary/rune value — technical data (FDID, Hash, CKey,
552- // Size) gets monospace + rune-blue per the design system; display
553- // fields (Name, Type, Locale) stay in the primary body color.
554- ui. label (
555- egui:: RichText :: new ( label)
556- . small ( )
557- . color ( rd:: FG_MUTED )
558- . strong ( ) ,
559- ) ;
563+ // Frost-variant `.v-meta`: rune-blue bold keys, primary values.
564+ // Technical fields (FDID, Hash, CKey, Size) render mono; display
565+ // fields (Name, Type, Locale) stay proportional.
566+ ui. label ( egui:: RichText :: new ( label) . strong ( ) . color ( rd:: RUNE_400 ) ) ;
560567 let is_technical = matches ! ( label, "FDID:" | "Hash:" | "CKey:" | "Size:" ) ;
561568 let max_chars = 24 ;
562569 let ( display, tip) : ( String , Option < & str > ) = if value. chars ( ) . count ( ) > max_chars {
563570 ( format ! ( "{}…" , & value[ ..max_chars] ) , Some ( value) )
564571 } else {
565572 ( value. to_string ( ) , None )
566573 } ;
567- let mut rt = egui:: RichText :: new ( & display) ;
574+ let mut rt = egui:: RichText :: new ( & display) . color ( rd :: FG_PRIMARY ) ;
568575 if is_technical {
569- rt = rt. monospace ( ) . color ( rd :: RUNE_400 ) ;
576+ rt = rt. monospace ( ) ;
570577 }
571578 let resp = ui. label ( rt) ;
572579 if let Some ( full) = tip {
0 commit comments