From 1a840e17d834cc5ccb40b504078b111b7bd00c8f Mon Sep 17 00:00:00 2001 From: Patrick O'Leary Date: Tue, 5 May 2026 12:33:35 -0500 Subject: [PATCH] refactor: slice selection toolbar with toggle groups - Each variable is now a UI group with a toggle button showing the track name - When toggled, reveals editable textfield and slider controls - Groups wrap to new rows with flex-wrap when space is limited - Value label shows bold (t_idx) when collapsed, italic value+units always - Simplified top_padding calculation (no multi-row pre-computation needed) - Use default density for components, darker group outline, larger slider --- src/e3sm_quickview/app.py | 16 +-- src/e3sm_quickview/components/toolbars.py | 159 +++++++++++----------- 2 files changed, 77 insertions(+), 98 deletions(-) diff --git a/src/e3sm_quickview/app.py b/src/e3sm_quickview/app.py index ff12d06..29f66ff 100644 --- a/src/e3sm_quickview/app.py +++ b/src/e3sm_quickview/app.py @@ -1,7 +1,6 @@ import asyncio import datetime import json -import math import os import time from functools import partial @@ -615,20 +614,7 @@ async def _on_projection(self, projection, **_): def _on_toolbar_change(self, active_tools, **_): top_padding = 0 for name in active_tools: - if name == "select-slice-time": - track_count = len(self.state.available_animation_tracks or []) - rows_needed = 1 - if track_count > 3: - if track_count % 3 == 0 or (track_count + 1) % 3 == 0: - rows_needed = math.ceil(track_count / 3) - elif track_count % 2 == 0: - rows_needed = track_count / 2 - else: - rows_needed = math.ceil(track_count / 3) - - top_padding += 70 * rows_needed - else: - top_padding += toolbars.SIZES.get(name, 0) + top_padding += toolbars.SIZES.get(name, 0) self.state.top_padding = top_padding diff --git a/src/e3sm_quickview/components/toolbars.py b/src/e3sm_quickview/components/toolbars.py index 9970760..37c6266 100644 --- a/src/e3sm_quickview/components/toolbars.py +++ b/src/e3sm_quickview/components/toolbars.py @@ -17,7 +17,7 @@ SIZES = { "adjust-layout": 49, "adjust-databounds": 65, - "select-slice-time": 70, + "select-slice-time": 65, "animation-controls": 49, } @@ -447,102 +447,95 @@ def __init__(self): ) super().__init__(**style) + self.state.setdefault("expanded_slice_track", None) + with self: - with v3.VTooltip( - text=( - "slice_slider_edit ? 'Toggle to text edit' : 'Toggle to slider edit'", - ), - ): - with v3.Template(v_slot_activator="{ props }"): - v3.VIcon( - "mdi-tune-variant", - v_bind="props", - classes="ml-3 mr-2 opacity-50", - click="slice_slider_edit = !slice_slider_edit", - ) + v3.VIcon("mdi-tune-variant", classes="ml-3 mr-2 opacity-50") - with v3.VRow( - classes="ma-0 pr-2 flex-wrap flex-grow-1", - dense=True, - v_if=("slice_slider_edit", True), + with html.Div( + classes="d-flex align-center flex-wrap flex-grow-1 ga-1 py-1 pr-2" ): - # Debug: Show animation_tracks array - # html.Div( - # "Animation Tracks: {{ JSON.stringify(available_animation_tracks) }}", - # classes="col-12", - # ) - # Each track gets a column (3 per row) - with v3.VCol( - cols=("utils.quickview.cols(available_animation_tracks.length)",), + with html.Template( v_for="(track, idx) in available_animation_tracks", key="idx", - classes="pa-2", ): with client.Getter(name=("track",), value_name="t_values"): with client.Getter( name=("track + '_idx'",), value_name="t_idx" ): - with v3.VRow(classes="ma-0 align-center", dense=True): - v3.VLabel( - "{{track}}", - classes="text-subtitle-2", - ) - v3.VSpacer() - v3.VLabel( - "{{ dim_units[track] ? parseFloat(t_values[t_idx]).toFixed(2) + ' ' + dim_units[track] : 'Index value: ' + t_idx }} (k={{ t_idx }})", - classes="text-body-2", - ) - v3.VSlider( - model_value=("t_idx",), - update_modelValue=( - self.on_update_slider, - "[track, $event]", + # --- Per-variable group --- + with v3.VSheet( + classes="d-flex align-center rounded px-1 ga-1", + color=( + "expanded_slice_track === track ? 'grey-lighten-3' : 'transparent'", ), - min=0, - # max=100,#("get(track.value).length - 1",), - max=("t_values.length - 1",), - step=1, - density="compact", - hide_details=True, - ) - with v3.VRow( - classes="ma-0 pl-6 pr-2 align-center ga-4", - v_if="!slice_slider_edit", - ): - with v3.VCol( - v_for="(track, idx) in available_animation_tracks", - key="idx", - ): - with client.Getter(name=("track",), value_name="t_values"): - with client.Getter( - name=("track + '_idx'",), value_name="t_idx" - ): - with v3.VRow(classes="ma-0 align-center", dense=True): - v3.VNumberInput( - model_value=("Number(t_idx)",), - update_modelValue=( - self.on_update_slider, - "[track, Number($event)]", - ), - key=("track + '_' + t_idx",), - min=[0], - max=["t_values ? t_values.length - 1 : 0"], - step=[1], - hide_details=True, - density="comfortable", - variant="plain", + style="border: 1px solid rgba(0,0,0,0.3) !important;", + ): + # Toggle button with track name + v3.VBtn( + "{{ track }}", + v_tooltip_bottom="'Toggle ' + track + ' controls'", flat=True, - control_variant="stacked", - style="max-width: 100px;", - reverse=True, - ) - v3.VLabel( - "{{track}}", - classes="text-subtitle-2 ml-2 mt-1", + variant=( + "expanded_slice_track === track ? 'flat' : 'outlined'", + ), + rounded=True, + click="expanded_slice_track = expanded_slice_track === track ? null : track", + color=( + "expanded_slice_track === track ? 'primary' : ''", + ), + style="text-transform: none;", ) + # Expanded controls + with ( + v3.VExpandXTransition(), + html.Div( + v_if="expanded_slice_track === track", + classes="d-flex align-center ga-1", + ), + ): + v3.VDivider(vertical=True, classes="mx-1") + # Text input + v3.VTextField( + model_value=("String(t_idx)",), + update_modelValue=( + self.on_update_slider, + "[track, Number($event)]", + ), + type="number", + min=[0], + max=["t_values ? t_values.length - 1 : 0"], + step=[1], + hide_details=True, + variant="outlined", + style="max-width: 80px;", + ) + # Slider + v3.VSlider( + v_tooltip_bottom=( + "dim_units[track] ? parseFloat(t_values[t_idx]).toFixed(2) + ' ' + dim_units[track] : 'Index: ' + t_idx", + ), + model_value=("t_idx",), + update_modelValue=( + self.on_update_slider, + "[track, $event]", + ), + min=0, + max=("t_values.length - 1",), + step=1, + show_ticks="always", + hide_details=True, + style="min-width: 200px; max-width: 400px;", + ) + # Value label v3.VLabel( - "{{ dim_units[track] ? parseFloat(t_values[Number(t_idx)]).toFixed(2) + ' ' + dim_units[track] : 'Index value: ' + t_idx }}", - classes="text-body-2 text-no-wrap ml-2 mt-1", + v_html=( + "(expanded_slice_track !== track ? '(' + t_idx + ')' : '')" + " + (dim_units[track] && isNaN(Number(dim_units[track]))" + " ? ', ' + parseFloat(t_values[t_idx]).toFixed(2) + ' ' + dim_units[track] + ''" + " : '')", + ), + style="opacity: 1; color: rgba(0,0,0,0.87);", ) def on_update_slider(self, dimension, index, *_, **__):