From a39bad260e7aef08ec8069f40435019cc6fb37b7 Mon Sep 17 00:00:00 2001
From: silverweed <silverweed14@proton.me>
Date: Thu, 13 Feb 2025 17:05:25 +0100
Subject: [PATCH] add section focus/expansion

---
 src/app_state.h   |  3 ++
 src/argparse.cpp  |  2 +-
 src/mainloop.cpp  | 14 +++++--
 src/render.cpp    | 99 ++++++++++++++++++++++++++++++++++++++---------
 src/render.h      | 12 ++++++
 src/rntviewer.cpp |  3 +-
 src/window.h      |  1 +
 7 files changed, 111 insertions(+), 23 deletions(-)

diff --git a/src/app_state.h b/src/app_state.h
index 150db72..5968e33 100644
--- a/src/app_state.h
+++ b/src/app_state.h
@@ -37,6 +37,9 @@ struct App_State {
 #endif
 
   String8 ntpl_name;
+  // This is used:
+  //   - in interactive mode: for aux section management
+  //   - in terminal mode: to know which is the first byte to display
   u64 base_display_addr;
 
   Delta_Time_Accum delta_time_accum;
diff --git a/src/argparse.cpp b/src/argparse.cpp
index f399733..6fc8263 100644
--- a/src/argparse.cpp
+++ b/src/argparse.cpp
@@ -12,7 +12,7 @@ void print_help(const char *argv0)
     "\n\t-l: display LEN bytes (only in terminal mode)"
     "\n\t-k: print information about the TKeys in the file and exit"
     "\n\t-n: list the names of the RNTuples found in the file and exit"
-    "\n\t-s: set first displayed byte to START"
+    "\n\t-s: set first displayed byte to START (only in terminal mode)"
     "\n\t-t: no graphics, output to terminal"
     "\n\t-v: print version and copyright info"
     "\n\t-w: display WIDTH bytes per column"
diff --git a/src/mainloop.cpp b/src/mainloop.cpp
index a44718f..0e6a22c 100644
--- a/src/mainloop.cpp
+++ b/src/mainloop.cpp
@@ -170,11 +170,18 @@ void run_main_loop(GLFWwindow *window, Arena *arena, App_State &app)
       init_viewer_title(app.viewer, app.fdata, app.inspected_file.name);
     }
 
-    if ((app.user_input.key_state[KEY_ESC] & KEY_STATE_IS_DOWN) || glfwWindowShouldClose(window)) {
-      app.should_quit = true; // superfluous right now, but set it just in case.
-      break;
+    if (app.user_input.key_state[KEY_ESC] & KEY_STATE_JUST_PRESSED) {
+      if (app.viewer.aux.buf_size) {
+        // We were viewing a subsection: back to the main view
+        app.viewer.aux.buf_size = app.base_display_addr = 0;
+      } else {
+        app.should_quit = true;
+      }
     }
 
+    if (glfwWindowShouldClose(window))
+      app.should_quit = true;
+
     b32 focused = glfwGetWindowAttrib(window, GLFW_FOCUSED);
     if (focused) {
       // NOTE: keyboard is updated via the callback because that's the only way to intercept GLFW_REPEAT events
@@ -186,6 +193,7 @@ void run_main_loop(GLFWwindow *window, Arena *arena, App_State &app)
       u8 *mouse_btn_state = app.user_input.mouse_btn_state;
       monitor_mouse_btn(window, mouse_btn_state, GLFW_MOUSE_BUTTON_LEFT,  MOUSE_BTN_LEFT);
       monitor_mouse_btn(window, mouse_btn_state, GLFW_MOUSE_BUTTON_RIGHT, MOUSE_BTN_RIGHT);
+      monitor_mouse_btn(window, mouse_btn_state, GLFW_MOUSE_BUTTON_MIDDLE, MOUSE_BTN_MIDDLE);
 
       app.user_input.mouse_pos.x = static_cast<i32>(mposx);
       app.user_input.mouse_pos.y = static_cast<i32>(mposy);
diff --git a/src/render.cpp b/src/render.cpp
index c01ab95..4e2552e 100644
--- a/src/render.cpp
+++ b/src/render.cpp
@@ -1,3 +1,7 @@
+// NOTE: we reserve more space than kMAXZIPBUF for the aux buf so we can fit also a TKey
+// or other similar pre-section in the buffer.
+#define AUX_BUF_SIZE (2 * kMAXZIPBUF)
+
 constexpr f32 max3(f32 a, f32 b, f32 c)
 {
   return (a > b) ? (a > c) ? a : (b > c) ? b : c : (b > c) ? b : c; 
@@ -98,10 +102,10 @@ internal
 u32 mem_edit_bg_color_fn(const u8 *data, u64 off, void *user_data)
 {
   App_State *app = reinterpret_cast<App_State *>(user_data);
-  off += app->base_display_addr;
+  u64 abs_off = off + app->base_display_addr;
 
   f32 brighten = 0;
-  if (app->viewer.hovered_range.start <= off && off < app->viewer.hovered_range.end())
+  if (app->viewer.hovered_range.start <= abs_off && abs_off < app->viewer.hovered_range.end())
     brighten = 0.3;
 
   u32 col = highlight_zstd_header(data, off, *app, brighten);
@@ -109,7 +113,12 @@ u32 mem_edit_bg_color_fn(const u8 *data, u64 off, void *user_data)
     return col;
 
   i64 hilite_cluster = app->viewer.highlight_cluster ? app->viewer.highlighted_cluster : -1;
-  Section section = find_section(*app, off, hilite_cluster);
+  Section section;
+  if (app->viewer.aux.buf_size && off > app->viewer.aux.section.pre_size) {
+    section = app->viewer.aux.section;
+  } else {
+    section = find_section(*app, abs_off, hilite_cluster);
+  }
   
   if (section.highlighted) 
     return imcol(app->viewer.col_highlight, brighten);
@@ -184,6 +193,9 @@ void init_viewer(App_State &app, u16 n_cols)
   viewer.mem_edit = make_memory_editor(app, (i32)n_cols);
   init_viewer_title(viewer, app.fdata, app.inspected_file.name);
 
+  // This lives as long as the viewer (i.e. forever)
+  viewer.aux.buf = (u8 *)malloc(AUX_BUF_SIZE);
+
 #define COL(c, r, g, b) viewer.c[0] = r/255.0, viewer.c[1] = g/255.0, viewer.c[2] = b/255.0
   COL(col_key,    0, 100, 50);
   COL(col_page_start, 200, 0, 200);
@@ -361,7 +373,12 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms)
         
     {
       f32 *c = app.viewer.col_title;
-      ImGui::TextColored(ImColor(c[0], c[1], c[2]), "%s", app.viewer.title.c());
+      String8 title = app.viewer.title;
+      if (app.viewer.aux.buf_size)
+        title = push_str8f(scratch.arena, "%s [viewing section \"%s\" at 0x%" PRIX64 "-0x%" PRIX64 "]", title.c(),
+                           section_names[app.viewer.aux.section.id].c(),
+                           app.base_display_addr, app.base_display_addr + app.viewer.aux.section_comp_size);
+      ImGui::TextColored(ImColor(c[0], c[1], c[2]), "%s", title.c());
     }
 
     // Draw stats
@@ -391,7 +408,7 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms)
 
       ImGui::BeginTable("Hex View", 2, ImGuiTableFlags_Resizable);
 
-      u64 content_size = app.inspected_file.size - app.base_display_addr;
+      u64 content_size = app.viewer.aux.buf_size ? app.viewer.aux.buf_size : app.inspected_file.size - app.base_display_addr;
       MemoryEditor::Sizes sizes;
       app.viewer.mem_edit.CalcSizes(sizes, content_size, app.base_display_addr);
 
@@ -406,16 +423,21 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms)
       // 0 means "invalid", otherwise the actual offset is `hovered_off - 1`.
       u64 hovered_off = app.viewer.mem_edit.MouseHovered * (app.viewer.mem_edit.MouseHoveredAddr + 1);
 
-      if (LIKELY(app.inspected_file.size)) {
-        assert(app.base_display_addr < app.inspected_file.size);
-        void *content = app.inspected_file.mem + app.base_display_addr;
-        app.last_pinfo = &invalid_pinfo;
-        app.last_other_root_obj = &invalid_section;
-        app.viewer.mem_edit.DrawContents(content, content_size, app.base_display_addr);
-      } else {
-        ImGui::Text("(File is empty)");
+      app.last_pinfo = &invalid_pinfo;
+      app.last_other_root_obj = &invalid_section;
+
+      void *content = nullptr;
+      if (app.viewer.aux.buf_size) {
+        content = app.viewer.aux.buf;
+      } else if (LIKELY(app.inspected_file.size)) {
+        content = app.inspected_file.mem;
       }
 
+      if (LIKELY(content))
+        app.viewer.mem_edit.DrawContents(content, content_size, 0);
+      else
+        ImGui::Text("(File is empty)");
+
       ImGui::TableNextColumn();
       const ImGuiColorEditFlags edit_flags =  ImGuiColorEditFlags_NoInputs|ImGuiColorEditFlags_NoLabel;
 
@@ -535,21 +557,30 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms)
 
       if (hovered_off)
       {
-        ImGui::TextColored(ImColor(0.f, 0.65f, 0.f), "Offset: 0x%" PRIX64, hovered_off - 1);
-        Section hovered_section = find_section(app, hovered_off - 1);
-        b8 hover_display_grouped = !(app.user_input.key_state[KEY_ALT] & KEY_STATE_IS_DOWN);
+        u64 abs_hovered_off = hovered_off - 1 + app.base_display_addr;
+        ImGui::TextColored(ImColor(0.f, 0.65f, 0.f), "Offset: 0x%" PRIX64, abs_hovered_off);
+
+        Section hovered_section = app.viewer.aux.buf_size ? app.viewer.aux.section : find_section(app, hovered_off - 1);
         Section_Id sec_id = hovered_section.id;
         // NOTE: anchor/header/footer sections all have their *info point to their parent rntuple anchor info.
         b8 old_version = 
           (sec_id == Sec_RNTuple_Anchor || sec_id == Sec_RNTuple_Header || sec_id == Sec_RNTuple_Footer) &&
           rntuple_is_old_version(((const RNTuple_Anchor_Info *)hovered_section.info)->anchor);
-        Sec_Hover_Info hover_info = get_section_hover_info(scratch.arena, hovered_section, hovered_off - 1, 
-                                                           app.inspected_file.mem, hover_display_grouped, old_version);
+        b8 hover_display_grouped = !(app.user_input.key_state[KEY_ALT] & KEY_STATE_IS_DOWN);
+        const u8 *section_info_data = app.viewer.aux.buf_size ? app.viewer.aux.buf : app.inspected_file.mem;
+        Sec_Hover_Info hover_info = get_section_hover_info(scratch.arena, hovered_section, hovered_off - 1,
+                                                           section_info_data, hover_display_grouped, old_version);
+
         ImGui::TextColored(ImColor(0.5f, 0.5f, 0.5f), "(Hint: hold Alt for single-field hover information)");
         ImGui::TextColored(ImColor(0.5f, 0.5f, 0.5f), "(Hint: Shift-Click to jump to the start of this section)");
+        if (!app.viewer.aux.buf_size)
+          ImGui::TextColored(ImColor(0.5f, 0.5f, 0.5f), "(Hint: Middle-Click to focus/expand current section)");
+        else
+          ImGui::TextColored(ImColor(0.5f, 0.5f, 0.5f), "(Hint: Escape to go back to full view)");
         if (hover_info.desc)
           imgui_render_string_tree(scratch.arena, hover_info.desc->head, hover_info.highlighted_desc);
         app.viewer.hovered_range = hover_info.rng;
+        app.viewer.hovered_range.start += !!app.viewer.aux.buf_size * app.base_display_addr;
 
         // Shift-clicking on a page section will update the current page in the legend
         if ((app.user_input.key_state[KEY_SHIFT] & KEY_STATE_IS_DOWN) &&
@@ -562,6 +593,38 @@ void update_and_render(Arena *arena, App_State &app, f32 delta_time_ms)
             viewer_jump_to(app, hovered_section.range.start);
           }
         }
+
+        //
+        // Handle section focusing/expanding.
+        // This feature isolates a single section and displays it uncompressed, allowing individual values hovering
+        // even for zipped payloads.
+        //
+        if (!app.viewer.aux.buf_size && (app.user_input.mouse_btn_state[MOUSE_BTN_MIDDLE] & MOUSE_BTN_STATE_JUST_PRESSED)) {
+          // Check if this section is compressed; decompress it if it's the case.
+          u8 *sec_buf = app.inspected_file.mem + hovered_section.range.start - hovered_section.pre_size;
+          auto comp = R__getCompressionAlgorithm(sec_buf + hovered_section.pre_size, hovered_section.range.len);
+          app.viewer.aux.section = hovered_section;
+          app.base_display_addr = hovered_section.range.start - hovered_section.pre_size;
+          app.viewer.aux.section.range.start = hovered_section.pre_size;
+          app.viewer.aux.section_comp_size = hovered_section.range.len + hovered_section.pre_size;
+          if (comp == ROOT::RCompressionSetting::EAlgorithm::kUndefined) {
+            // Not a compressed block: just copy it to the aux buffer.
+            app.viewer.aux.buf_size = hovered_section.range.len + hovered_section.pre_size;
+            memcpy(app.viewer.aux.buf, sec_buf, min(AUX_BUF_SIZE, app.viewer.aux.buf_size));
+          } else {
+            // Copy over the pre-section (usually the TKey)
+            memcpy(app.viewer.aux.buf, sec_buf, hovered_section.pre_size);
+
+            // Decompress the block
+            i32 src_size = hovered_section.range.len;
+            i32 tgt_size = kMAXZIPBUF;
+            i32 unzipped_nbytes;
+            R__unzip(&src_size, sec_buf + hovered_section.pre_size, &tgt_size,
+                     app.viewer.aux.buf + hovered_section.pre_size, &unzipped_nbytes);
+            app.viewer.aux.buf_size = hovered_section.pre_size + unzipped_nbytes;
+            app.viewer.aux.section.range.len = unzipped_nbytes;
+          }
+        }
       } else {
         app.viewer.hovered_range = {};
       }
diff --git a/src/render.h b/src/render.h
index a1d05b9..26a2e3c 100644
--- a/src/render.h
+++ b/src/render.h
@@ -1,3 +1,13 @@
+// Contains the data of the currently focused/expanded section (middle-click on the section)
+struct Viewer_Aux_Section {
+  // This is an allocation AUX_BUF_SIZE wide.
+  u8 *buf;
+  // The aux section (i.e. all the data inside this struct) is considered valid if and only if this is > 0
+  u64 buf_size;
+  Section section;
+  u64 section_comp_size;
+};
+
 struct Viewer {
     // Imgui colors
   f32 col_section[Sec_COUNT][3];
@@ -27,6 +37,8 @@ struct Viewer {
 
   // maps glfw keys to our keys
   Input_Key glfw_key_mapping[GLFW_KEY_LAST];
+
+  Viewer_Aux_Section aux;
 #endif
 };
 
diff --git a/src/rntviewer.cpp b/src/rntviewer.cpp
index ebed1e3..2d711d3 100644
--- a/src/rntviewer.cpp
+++ b/src/rntviewer.cpp
@@ -158,7 +158,8 @@ int main(int argc, char **argv)
   }
 
   app.ntpl_name = args.ntpl_name; // may be null
-  app.base_display_addr = args.start_addr;
+  if (!is_interactive)
+    app.base_display_addr = args.start_addr;
   u32 walk_tkeys_flags = WTK_NONE;
   if (args.print_keys_info)
     walk_tkeys_flags |= WTK_PRINT_KEYS_INFO;
diff --git a/src/window.h b/src/window.h
index 147fbe7..6799d07 100644
--- a/src/window.h
+++ b/src/window.h
@@ -25,6 +25,7 @@ enum Key_State : u8 {
 enum Mouse_Button {
   MOUSE_BTN_LEFT,
   MOUSE_BTN_RIGHT,
+  MOUSE_BTN_MIDDLE,
   MOUSE_BTN_COUNT
 };