@@ -886,124 +886,114 @@ class _LogViewerState extends State<LogViewer> {
886886 final contentWidth = _contentWidth (viewportWidth);
887887 final logViewport = _buildLogViewport (messageWidth);
888888
889- return Stack (
889+ final innerColumn = Column (
890890 children: [
891- Scrollbar (
892- controller: _horizontalScrollController,
893- thumbVisibility: true ,
894- notificationPredicate: (notification) =>
895- notification.metrics.axis == Axis .horizontal,
896- child: SingleChildScrollView (
897- controller: _horizontalScrollController,
898- scrollDirection: Axis .horizontal,
899- child: SizedBox (
900- width: contentWidth,
901- child: Column (
902- children: [
903- _buildHeader (messageWidth),
904- const Divider (height: 1 , thickness: 1 ),
905- Expanded (
906- child: NotificationListener <ScrollNotification >(
907- onNotification: (notification) {
908- if (notification.metrics.axis != Axis .vertical) {
909- return false ;
910- }
911- final isUserScroll =
912- notification is UserScrollNotification ||
913- notification is ScrollStartNotification &&
914- notification.dragDetails != null ||
915- notification is ScrollUpdateNotification &&
916- notification.dragDetails != null ||
917- notification is OverscrollNotification &&
918- notification.dragDetails != null ;
919- if (isUserScroll) {
920- widget.onUserScroll? .call ();
921- }
922- return false ;
891+ _buildHeader (messageWidth),
892+ const Divider (height: 1 , thickness: 1 ),
893+ Expanded (
894+ child: NotificationListener <ScrollNotification >(
895+ onNotification: (notification) {
896+ if (notification.metrics.axis != Axis .vertical) return false ;
897+ final isUserScroll =
898+ notification is UserScrollNotification ||
899+ (notification is ScrollStartNotification &&
900+ notification.dragDetails != null ) ||
901+ (notification is ScrollUpdateNotification &&
902+ notification.dragDetails != null ) ||
903+ (notification is OverscrollNotification &&
904+ notification.dragDetails != null );
905+ if (isUserScroll) widget.onUserScroll? .call ();
906+ return false ;
907+ },
908+ child: GestureDetector (
909+ onScaleStart: (details) {
910+ _scaleBaseFontSize = PreferencesService .logFontSize;
911+ },
912+ onScaleUpdate: (details) {
913+ if (details.pointerCount < 2 ) return ;
914+ final base =
915+ _scaleBaseFontSize ?? PreferencesService .logFontSize;
916+ PreferencesService .logFontSize = (base * details.scale)
917+ .roundToDouble ();
918+ },
919+ onScaleEnd: (_) => _scaleBaseFontSize = null ,
920+ child: Listener (
921+ onPointerDown: widget.rowSelectionMode
922+ ? null
923+ : (event) {
924+ if ((event.buttons & kPrimaryButton) == 0 ) return ;
925+ widget.onLogRowTap? .call ();
923926 },
924- child: Scrollbar (
925- controller: widget.scrollController,
926- thumbVisibility: true ,
927- child: GestureDetector (
928- // Support pinch-to-zoom on trackpads / touchpads to change log font size.
929- onScaleStart: (details) {
930- // Record the base font size at gesture start.
931- _scaleBaseFontSize =
932- PreferencesService .logFontSize;
933- },
934- onScaleUpdate: (details) {
935- // Only react when there are multiple pointers (pinch gesture).
936- if (details.pointerCount < 2 ) return ;
937- final base =
938- _scaleBaseFontSize ??
939- PreferencesService .logFontSize;
940- final target = base * details.scale;
941- // Use integer steps to avoid jitter; PreferencesService
942- // will clamp and avoid redundant writes.
943- final rounded = target.roundToDouble ();
944- PreferencesService .logFontSize = rounded;
945- },
946- onScaleEnd: (_) {
947- _scaleBaseFontSize = null ;
948- },
949- child: Listener (
950- onPointerDown: widget.rowSelectionMode
951- ? null
952- : (event) {
953- if ((event.buttons & kPrimaryButton) ==
954- 0 ) {
955- return ;
956- }
957- widget.onLogRowTap? .call ();
958- },
959- onPointerUp: (event) =>
960- _endRowSelectionDrag (event.pointer),
961- onPointerCancel: (event) =>
962- _endRowSelectionDrag (event.pointer),
963- child: Theme (
964- data: widget.rowSelectionMode
965- ? Theme .of (context).copyWith (
966- textSelectionTheme:
967- const TextSelectionThemeData (
968- selectionColor:
969- Colors .transparent,
970- ),
971- )
972- : Theme .of (context),
973- child: SelectionArea (
974- key: const ValueKey (
975- 'log-viewer-selection-area' ,
976- ),
977- onSelectionChanged: (selectedContent) {
978- widget.onSelectedTextChanged? .call (
979- selectedContent? .plainText,
980- );
981- },
982- contextMenuBuilder:
983- (ctx, selectableRegionState) =>
984- _buildSelectionContextMenu (
985- ctx,
986- selectableRegionState,
987- ),
988- child: logViewport,
989- ),
990- ),
927+ onPointerUp: (event) => _endRowSelectionDrag (event.pointer),
928+ onPointerCancel: (event) =>
929+ _endRowSelectionDrag (event.pointer),
930+ child: Theme (
931+ data: widget.rowSelectionMode
932+ ? Theme .of (context).copyWith (
933+ textSelectionTheme: const TextSelectionThemeData (
934+ selectionColor: Colors .transparent,
991935 ),
936+ )
937+ : Theme .of (context),
938+ child: SelectionArea (
939+ key: const ValueKey ('log-viewer-selection-area' ),
940+ onSelectionChanged: (selectedContent) {
941+ widget.onSelectedTextChanged? .call (
942+ selectedContent? .plainText,
943+ );
944+ },
945+ contextMenuBuilder: (ctx, selectableRegionState) =>
946+ _buildSelectionContextMenu (
947+ ctx,
948+ selectableRegionState,
992949 ),
993- ),
994- ),
950+ child: logViewport,
995951 ),
996- ] ,
952+ ) ,
997953 ),
998954 ),
999955 ),
1000956 ),
1001- // Sticky column-visibility button pinned to the top-right corner.
1002- // It stays in place regardless of horizontal scroll.
957+ ],
958+ );
959+
960+ // Wrap SingleChildScrollView in a ScrollConfiguration that disables
961+ // ALL ambient scrollbars. This prevents Flutter's ScrollBehavior from
962+ // auto-painting a second ghost thumb on desktop/web — our explicit
963+ // Scrollbar widgets above are the only ones that should render.
964+ final scrollableContent = ScrollConfiguration (
965+ behavior: ScrollConfiguration .of (context).copyWith (scrollbars: false ),
966+ child: SingleChildScrollView (
967+ controller: _horizontalScrollController,
968+ scrollDirection: Axis .horizontal,
969+ child: SizedBox (width: contentWidth, child: innerColumn),
970+ ),
971+ );
972+
973+ return Stack (
974+ children: [
975+ // Vertical scrollbar — pinned to screen right, vertical axis only.
976+ Scrollbar (
977+ controller: widget.scrollController,
978+ thumbVisibility: true ,
979+ notificationPredicate: (n) => n.metrics.axis == Axis .vertical,
980+ // Horizontal scrollbar — pinned to screen bottom, shown only when
981+ // content actually overflows (i.e. wrapText == false).
982+ child: widget.wrapText
983+ ? scrollableContent
984+ : Scrollbar (
985+ controller: _horizontalScrollController,
986+ thumbVisibility: true ,
987+ notificationPredicate: (n) =>
988+ n.metrics.axis == Axis .horizontal,
989+ child: scrollableContent,
990+ ),
991+ ),
992+ // Column-visibility button — always pinned top-right.
1003993 Positioned (
1004994 top: 0 ,
1005995 right: 0 ,
1006- height: 29 , // match header + divider height (28 + 1)
996+ height: 29 ,
1007997 child: Container (
1008998 padding: const EdgeInsets .symmetric (horizontal: 8 ),
1009999 color: Theme .of (context).colorScheme.surfaceContainerHighest,
@@ -1016,7 +1006,6 @@ class _LogViewerState extends State<LogViewer> {
10161006 color: Theme .of (context).colorScheme.onSurfaceVariant,
10171007 ),
10181008 onTap: () {
1019- // Show popup anchored to the button's position.
10201009 final renderBox =
10211010 context.findRenderObject () as RenderBox ? ;
10221011 final offset =
@@ -1073,7 +1062,8 @@ class _LogViewerState extends State<LogViewer> {
10731062 allowSelectionStart: log.isUserSelectable,
10741063 onSelectionPointerDown: (event) => _startRowSelectionDrag (index, event),
10751064 onSelectionPointerMove: (event) => _extendRowSelectionDrag (index, event),
1076- contentValueForColumn: (col) => log.valueForColumn (col, isIos: widget.isIos),
1065+ contentValueForColumn: (col) =>
1066+ log.valueForColumn (col, isIos: widget.isIos),
10771067 );
10781068 }
10791069
0 commit comments