caduandrade / multi_split_view

Provides horizontal or vertical multiple split view for Flutter.
https://caduandrade.github.io/multi_split_view/
MIT License
129 stars 24 forks source link

Flex with minimum size #76

Open AtlasAutocode opened 3 days ago

AtlasAutocode commented 3 days ago

I am upgrading from version 2.

The following settings result in 3 areas but the dividers do not work to resize the panes.

    _controller.areas = [
      Area(data: _randomColor(), size: 600, min: 100),
      Area(data: _randomColor(), size: 150, min: 100, max: 400),
      Area(data: _randomColor(), size: 150, min: 100),
    ];

If I add in a flex area, now the dividers act to resize. The size areas obey the min and max settings. But I cannot set a minimum size for the flex area. Adding min or max to the flex area has no effect.

    _controller.areas = [
      Area(data: _randomColor(), size: 600, min: 100),
      Area(data: _randomColor(), flex: 1 ),
      Area(data: _randomColor(), size: 150, min: 100, max: 400),
      Area(data: _randomColor(), size: 150, min: 100),
    ];

What am I doing wrong? How do I set a minimum size for a flex area?

caduandrade commented 2 days ago

Hi @AtlasAutocode!

The size(pixel) and flex are independent. The flex is applied to the space not used by "size". So in your example, it isn't working because you only have one flex area.

It's worth remembering that the min in flex is not in pixels.

AtlasAutocode commented 2 days ago

The problem is that if I don't include a flex then resizing does not work. The dividers do not work.

3 'size' areas => no resizing, moving the divider has no effect. Areas are frozen and cannot be adjusted with the divider.

If I add 1 flex then I get resizing of all 4 areas using the divider - but I then cannot set a minimum size for the flex area.

My app needs 3 'size' areas that have a min size - how can I get the dividers to work?

Yes, I understand that the min in flex is flex but I still need to set the minimum size in pixels. Are you saying that every time the main window changes size, I have to recalculate the min flex?

I think resizing should work with 3 size areas.

caduandrade commented 1 day ago

I used your configuration in the committed example, and it works as expected: fixed sizes in pixels will not allow resizing depending on the size of your window and whether it has already reached a limit (max or min). Pay attention to the max limit too, for one area to shrink, the other has to stretch.

The flex is in proportion to the window size like Column/Row. Min follows the same rule. It cannot be in pixels.

If you want to adjust the min flex based on pixels, you will have to adjust the calculation using a LayoutBuilder to know the size of the window. Remember that flex uses the window size by first removing the fixed pixels used. What is left is used in the value of your factors. This flex and pixel scenario can generate inconsistencies, these are handled when they occur (you can see the demo). Example: you define 2 areas with 500 pixels each but your window is smaller.

It seems strange to me that you use all areas in pixels because the window can resize. The pixel is most useful for pinning a single area, such as a side menu.

For now, there is no way to set the flex min in pixels. It's either flex or sized (pixels).

caduandrade commented 1 day ago

If it were possible to define the min in pixels for the flex area, there would have to be 2 options. Like minFlex and minPixels. But perhaps it generates new inconsistencies.

AtlasAutocode commented 1 day ago

Version 2 did allow defining 3 areas whose starting size could be set with min and max values. All 3 areas could be resized, and min size was followed.

It sounds like version 3 has changed to use size areas as fixed (not resizable) areas and min/max are ignored. That is disappointing.

caduandrade commented 1 day ago

Sized areas are resizable as long as there is space for it in the window. You may have a problem in your code or the window does not have enough space.

Screenshot_20240705_110854 Screenshot_20240705_110916

With your configuration:

import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:multi_split_view/multi_split_view.dart';

void main() => runApp(const MultiSplitViewExampleApp());

class MultiSplitViewExampleApp extends StatelessWidget {
  const MultiSplitViewExampleApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MultiSplitViewExample(),
    );
  }
}

class MultiSplitViewExample extends StatefulWidget {
  const MultiSplitViewExample({Key? key}) : super(key: key);

  @override
  MultiSplitViewExampleState createState() => MultiSplitViewExampleState();
}

class MultiSplitViewExampleState extends State<MultiSplitViewExample> {
  final MultiSplitViewController _controller = MultiSplitViewController();

  bool _pushDividers = false;

  @override
  void initState() {
    super.initState();
    _controller.areas = [
      Area(data: _randomColor(), size: 600, min: 100),
      Area(data: _randomColor(), size: 150, min: 100, max: 400),
      Area(data: _randomColor(), size: 150, min: 100)
    ];
    _controller.addListener(_rebuild);
  }

  @override
  void dispose() {
    super.dispose();
    _controller.removeListener(_rebuild);
  }

  void _rebuild() {
    setState(() {
      // rebuild to update empty text and buttons
    });
  }

  @override
  Widget build(BuildContext context) {
    Widget buttons = Container(
        color: Colors.white,
        padding: const EdgeInsets.all(8),
        child: Wrap(
            crossAxisAlignment: WrapCrossAlignment.center,
            spacing: 10,
            runSpacing: 10,
            children: [
              ElevatedButton(
                  onPressed: _onAddFlexButtonClick,
                  child: const Text('Add flex')),
              ElevatedButton(
                  onPressed: _onAddSizeButtonClick,
                  child: const Text('Add size')),
              ElevatedButton(
                  onPressed: _controller.areasCount != 0
                      ? _onRemoveFirstButtonClick
                      : null,
                  child: const Text('Remove first')),
              Checkbox(
                  value: _pushDividers,
                  onChanged: (newValue) => setState(() {
                        _pushDividers = newValue!;
                      })),
              const Text("Push dividers")
            ]));

    Widget? content;
    if (_controller.areasCount != 0) {
      MultiSplitView multiSplitView = MultiSplitView(
          onDividerDragUpdate: _onDividerDragUpdate,
          onDividerTap: _onDividerTap,
          onDividerDoubleTap: _onDividerDoubleTap,
          controller: _controller,
          pushDividers: _pushDividers,
          builder: (BuildContext context, Area area) => ColorWidget(
              area: area, color: area.data, onRemove: _removeColor));

      content = Padding(
          padding: const EdgeInsets.all(16),
          child: MultiSplitViewTheme(
              data: MultiSplitViewThemeData(
                  dividerPainter: DividerPainters.grooved2()),
              child: multiSplitView));
    } else {
      content = const Center(child: Text('Empty'));
    }

    return Scaffold(
        appBar: AppBar(title: const Text('Multi Split View Example')),
        body: Column(children: [buttons, Expanded(child: content)])
        // body: horizontal,
        );
  }

  Color _randomColor() {
    Random random = Random();
    return Color.fromARGB(255, 155 + random.nextInt(100),
        155 + random.nextInt(100), 155 + random.nextInt(100));
  }

  _onDividerDragUpdate(int index) {
    if (kDebugMode) {
      // print('drag update: $index');
    }
  }

  _onRemoveFirstButtonClick() {
    if (_controller.areasCount != 0) {
      _controller.removeAreaAt(0);
    }
  }

  _onDividerTap(int dividerIndex) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      duration: const Duration(seconds: 1),
      content: Text("Tap on divider: $dividerIndex"),
    ));
  }

  _onDividerDoubleTap(int dividerIndex) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      duration: const Duration(seconds: 1),
      content: Text("Double tap on divider: $dividerIndex"),
    ));
  }

  _onAddFlexButtonClick() {
    _controller.addArea(Area(data: _randomColor()));
  }

  _onAddSizeButtonClick() {
    _controller.addArea(Area(data: _randomColor(), size: 100));
  }

  void _removeColor(int index) {
    _controller.removeAreaAt(index);
  }
}

class ColorWidget extends StatelessWidget {
  const ColorWidget(
      {Key? key,
      required this.color,
      required this.onRemove,
      required this.area})
      : super(key: key);

  final Color color;
  final Area area;
  final void Function(int index) onRemove;

  @override
  Widget build(BuildContext context) {
    List<Widget> children = [];
    TextStyle textStyle = const TextStyle(fontSize: 10);
    if (area.size != null) {
      children.add(Text('size: ${area.size!}', style: textStyle));
    }
    if (area.flex != null) {
      children.add(Text('flex: ${area.flex!}', style: textStyle));
    }
    if (area.min != null) {
      children.add(Text('min: ${area.min!}', style: textStyle));
    }
    if (area.max != null) {
      children.add(Text('max: ${area.max!}', style: textStyle));
    }
    Widget info = Center(
        child: Container(
            color: const Color.fromARGB(200, 255, 255, 255),
            padding: const EdgeInsets.fromLTRB(3, 2, 3, 2),
            child: Wrap(
                runSpacing: 5,
                spacing: 5,
                crossAxisAlignment: WrapCrossAlignment.center,
                children: children)));

    return InkWell(
        onTap: () => onRemove(area.index),
        child: Container(
            color: color,
            child: Stack(
                children: [const Placeholder(color: Colors.black), info])));
  }
}
AtlasAutocode commented 1 day ago

I copied your code. I get 3 areas of width: 600, 150, 464.28 The dividers do not resize the first 2 areas. I can change the width of the main window and area3 resizes. When it becomes 0, area2 shrinks. The dividers never work.

Sized areas are resizable as long as there is space for it in the window

But not resizable by the user.

caduandrade commented 1 day ago

The initial size of 464.28 and the shrinking behavior is as expected given the configurable inconsistency handling.

Regarding the divider that doesn't move, it seems to me that it's another problem. Maybe it's a mouse event that isn't even being triggered. Maybe something related to your Flutter or OS version? You could download the source and test it by debugging. Check whether the _onDragDown and _onDragUpdate methods are being executed. They are in multi_split_view.dart. I test with Web and Linux. I don't expect Flutter to need different codes given the OS but it's worth investigating.

You will notice that within _onDragUpdate, there is a call to DividerUtil.move which has the logic to calculate the appropriate resizing.

AtlasAutocode commented 1 day ago

I cloned your repo. Running example code - all areas resizable. Remove the flex area - dividers do not move. Both on Windows and Edge browser.

In all cases _onDrawDown and _onDrawUpdate are called even when the divider does not work. _onDrawDown creates _draggingDivider _onDrawUpdate does nothing because _draggingDivider == null

L185 sets _draggingDivider = null; called because: _lastAreasUpdateHash != controllerHelper.areasUpdateHash forces loss of divider hence divider does not work.

Restoring the flex area, now the UpdateHashes match and dividers work.

Running Dart 3.4.3, Flutter 3.22.2

caduandrade commented 1 day ago

:thinking:

This line L185 resets the _draggingDivider in situations such as the window was resized, the theme was changed or some area was added/removed. So it makes sense to cancel the divider drag. But the first time the window opens, this code is executed and _lastAreasUpdateHash should be set with:

_lastAreasUpdateHash = controllerHelper.areasUpdateHash;

This areasUpdateHash is just an Object to indicate that an area has been added or removed. Every method that adds or removes areas creates a new Object.

I don't understand how on your machine it only works with flex. The hash is not related to flex or sized.

When you don't use flex, the divider drag always goes into the build method and L185 because of the _lastAreasUpdateHash != controllerHelper.areasUpdateHash comparison?

It doesn't make sense because then we have:

 _lastAreasUpdateHash = controllerHelper.areasUpdateHash;

Could you check if L139 (layout_constraints.dart) is also executed? This could create a new hash and keep this loop. This L139 is the controllerHelper.updateAreas(); inside the adjustAreas method.

      if (_lastAreasUpdateHash != controllerHelper.areasUpdateHash ||
          _layoutConstraints == null ||
          _layoutConstraints!.containerSize != containerSize ||
          _layoutConstraints!.dividerThickness != themeData.dividerThickness) {
        _draggingDivider = null;

                  /// KEEP HASH HERE
        _lastAreasUpdateHash = controllerHelper.areasUpdateHash;

        _layoutConstraints = LayoutConstraints(
            controller: _controller,
            containerSize: containerSize,
            dividerThickness: themeData.dividerThickness);

            /// BUT MAYBE RESET controllerHelper.areasUpdateHash HERE
        _layoutConstraints!.adjustAreas(
            controllerHelper: controllerHelper,
            sizeOverflowPolicy: widget.sizeOverflowPolicy,
            sizeUnderflowPolicy: widget.sizeUnderflowPolicy,
            minSizeRecoveryPolicy: widget.minSizeRecoveryPolicy);
      }

If you enter L139, you need to find out why changed is TRUE

if (changed) {
 controllerHelper.updateAreas();
 Future.microtask(() => controllerHelper.notifyListeners());
 }
AtlasAutocode commented 1 day ago

L139 (layout_constraints.dart) is continuously executed

Reason: L117 is true because (totalSize < spaceForAreas && flexCount == 0) L128 adjusts the size by spaceForAreas - totalSize. This has a value: 2.87 e-7 !!! L136 changed = true forces redraw

It appears that the extremely small adjustment has no effect on the reported sizes and forces a continuous loop. There are several places in the function where adjustment is calculated as too tiny.

If I add a statement like

if ( spaceForAreas - totalSize < 1 ) {
  return;
}

It now works

As an additional comment SizeUnderflowPolicy.stretchAll is best for my use SizeOverflowPolicy - would be good to have 'shrinkAll'

caduandrade commented 21 hours ago

This wrong value is because of a floating point. :disappointed:

Bug issue: #78 shrinkAll: #77