4
$\begingroup$

Having a Pane with AppearanceElements->{"ResizeArea"} is super convenient. It's nice to be able to adjust sizes as needed.

Even better, we can do something like:

is = Automatic;
Pane["asdasd",
 Dynamic[is],
 AppearanceElements -> {"ResizeArea"}
 ]

or

is = {100, 100};
Pane["asdasd",
 Dynamic[is, Set[is[[2]], #[[2]]] &],
 AppearanceElements -> {"ResizeArea"}
 ]

And be able to know how big our Pane is.

Unfortunately, the "ResizeArea" is ugly and only applies to Pane. Can we make a modern resize area, like the standard resize edge that most modern OS use?

$\endgroup$
3
$\begingroup$

The basic trick is just to re-implement a resize area in top-level code. Here's a way to do that:

Options[resizeArea] =
  Join[
   Options[Graphics],
   {
    MouseAppearance -> Automatic
    }
   ];
resizeArea[
   {
    Verbatim[Dynamic][size_,
      {__, f_} | f : Except[_?OptionQ],
      ___?OptionQ
      ] |
     Verbatim[Dynamic][size_,
      ___?OptionQ
      ],
    Verbatim[Dynamic][dragBase_, ___],
    Verbatim[Dynamic][sizeBase_, ___]
    },
   mode : "Vertical" | "Horizontal" | "Both" : "Both",
   graphics : _List?(Not@*OptionQ) | {} : {},
   ops : OptionsPattern[]
   ] :=
  Deploy@EventHandler[
    MouseAppearance[#,
       Replace[OptionValue[MouseAppearance],
        Automatic :>
         Switch[mode,
          "Vertical", "FrameTBResize",
          "Horizontal", "FrameLRResize",
          "Both", "DragAndDrop"
          ]
        ]
       ] &@
     Graphics[
      graphics,
      FilterRules[{
        ImageSize ->
         Replace[OptionValue[ImageSize],
          Automatic :>
           If[graphics === {},
            Switch[mode,
             "Vertical", {Full, 2},
             "Horizontal", {2, Full},
             "Both", {22, 22}
             ],
            Automatic
            ]
          ],
        ops,
        Background -> GrayLevel[.95],
        Frame -> True,
        FrameStyle -> GrayLevel[.8],
        FrameMargins -> None,
        AspectRatio -> Full,
        ImagePadding -> -1,
        PlotRangePadding -> 0,
        Method -> {"ShrinkWrap" -> True},
        ImageMargins -> -1
        },
       Options@Graphics
       ]
      ], {
     "MouseDown" :>
      (
       If[! (

          NumericQ@dragBase ||

           AllTrue[Flatten@{dragBase}, NumericQ]
          ),
        Replace[MousePosition["ScreenAbsolute"], {
          {x_, y_} :>
           Switch[mode,
            "Vertical", Set[dragBase, y],
            "Horizontal", Set[dragBase, x],
            "Both", Set[dragBase, {x, y}]
            ]
          }]
        ];
       If[! (

          NumericQ@sizeBase ||

           AllTrue[Flatten@{sizeBase}, NumericQ]
          ),
        If[! (

           NumericQ@size ||

            AllTrue[Flatten@{size}, NumericQ]
           ),
         size = dragBase
         ];
        sizeBase = size
        ]
       ),
     "MouseUp" :>
      Clear[dragBase, sizeBase],
     "MouseDragged" :>
      (
       Replace[MousePosition["ScreenAbsolute"],
        {x_, y_} :>
         With[{m =
            Switch[mode,
             "Vertical", y,
             "Horizontal", x,
             "Both", {x, y}
             ]
           },
          If[! (

             NumericQ@dragBase ||

              AllTrue[Flatten@{dragBase}, NumericQ]
             ),
           Set[dragBase, m];
           sizeBase = size,
           With[{new = sizeBase +  m - dragBase},
            If[Length@{f} === 0,
             Set[size,
              If[ListQ@size,
               Switch[mode,
                "Both", new,
                "Vertical", {size[[1]], new[[2]]},
                "Horizontal", {new[[1]], size[[2]]}
                ],
               new
               ]
              ],
             f[new]
             ]
            ]
           ]
          ]
        ]
       ),
     PassEventsDown -> True
     }];

Options[ResizeArea] =
  Join[
   Options@resizeArea,
   {
    "Orientation" -> "Both"
    }
   ];
ResizeArea[d_Dynamic,
   graphics : Except[_?OptionQ] : {},
   ops : OptionsPattern[]
   ] :=
  DynamicModule[{tmp1, tmp2},
   resizeArea[
    {d, Dynamic[tmp1], Dynamic[tmp2]},
    Replace[OptionValue["Orientation"],
     Except["Horizontal" | "Vertical"] -> "Both"
     ],
    Replace[graphics, {
      i_?ImageQ :> {Inset[i]},
      Except[_List] -> {graphics}
      }],
    FilterRules[{ops}, Options@resizeArea]
    ],
   UnsavedVariables :> {tmp1, tmp2}
   ];

We can now have resize grabs of our liking. For instance, we could make a resize grab that resizes itself, with a Dynamic image inset:

x = {100, 100};
ResizeArea[
 Dynamic[x],
 Inset[
  Dynamic[
   LinearGradientImage[
    {{Left, Top}, {Right, Bottom}} -> {Yellow, Red, Blue},
    Floor@x
    ]
   ],
  Center,
  Center,
  Scaled[1]
  ],
 ImageSize -> Dynamic[x]
 ]

This resizes x which in turn resizes the grab:

doop movie

In this we also have control over whether we want a horizontal or vertical resizer. We'll use these to build a Pane with resize edges and corners:

Options[ResizePane] =
  Join[
   Options[Pane],
   Options[resizeArea],
   Options[Grid],
   {
    "ResizeLocations" -> {Right, Bottom},
    "ResizeCorners" -> True,
    "ResizeEdges" -> True,
    "ResizeAreaSize" -> 1,
    "ResizeCornerPadding" -> 2,
    "ResizeAreaOptions" -> {FrameTicks -> None}
    }
   ];
ResizePane[expr_,
  Optional[var : Verbatim[Dynamic][imSize_, e___] | None, None],
  ops : OptionsPattern[]
  ] :=
 DynamicModule[
  {
   imSizeVar,
   imsx,
   imsy,
   ims,
   tmp1,
   tmp2,
   resizeAreaSize =
    Replace[OptionValue["ResizeAreaSize"], {
      {w_, h_} :>
       {
        Replace[w, Except[_Integer?Positive] -> 1],
        Replace[h, Except[_Integer?Positive] -> 1]
        },
      Except[_Integer?Positive] -> {1, 1},
      i_Integer?Positive :> {i, i}
      }],
   cornerPad =
    Replace[OptionValue["ResizeCornerPadding"],
     Except[_Integer] -> 0
     ],
   rops =
    Flatten@{
      Replace[OptionValue["ResizeAreaOptions"],
       Except[_?OptionQ] -> {}
       ]
      },
   le = MemberQ[OptionValue["ResizeLocations"], Left],
   te = MemberQ[OptionValue["ResizeLocations"], Top],
   re = MemberQ[OptionValue["ResizeLocations"], Right],
   be = MemberQ[OptionValue["ResizeLocations"], Bottom],
   useEdges = MatchQ[OptionValue["ResizeEdges"], Except[False]],
   useCorners = MatchQ[OptionValue["ResizeCorners"], Except[False]],
   calr,
   catb
   },
  calr = Which[le && re, Center, le, Right, re, Left, True, Center];
  catb = Which[te && be, Center, te, Bottom, be, Top, True, Center];
  imSizeVar =
   If[var === None,
    Dynamic[ims,
     Set[ims, {Max[{#[[1]], 1}], Max[{#[[2]], 1}]}] &
     ],
    Dynamic[imSize,
     Set[imSize, {Max[{#[[1]], 1}], Max[{#[[2]], 1}]}] &
     ]
    ];
  Extract[imSizeVar, 1,
   Function[Null,
    Set[#,
     Replace[OptionValue[ImageSize], {
       Automatic :>
        Replace[{imSize}, {
          {i_?NumericQ} :> {Max@{i, 1}, Max@{i, 1}},
          {{x_?NumericQ, y_?NumericQ}} :> {Max@{x, 1}, Max@{y, 1}},
          _ -> {100, 50}
          }],
       i : {_, _} :> i,
       i_ :> {Replace[i, Automatic :> 100], 
         Replace[i, Automatic :> 50]}
       }
      ]
     ],
    HoldAllComplete
    ]
   ];
  imsx =
   Extract[imSizeVar, 1,
    Function[Null,
     Replace[HoldComplete[#],
      HoldComplete[v_] :>

       Dynamic[v[[1]], Set[v[[1]], Max@{#, 1}] &]
      ],
     HoldAllComplete]
    ];
  imsy =
   Extract[imSizeVar, 1,
    Function[Null,
     Replace[HoldComplete[#],
      HoldComplete[v_] :>

       Dynamic[v[[2]], Set[v[[2]], Max@{#, 1}] &]
      ],
     HoldAllComplete]
    ];
  Grid[{
    If[te,
     {
      If[le,
       If[useCorners,
        Item[#, Alignment -> {Left, Top}] &@
         resizeArea[
          {
           imSizeVar,
           Dynamic[tmp1],
           Dynamic[tmp2]
           },
          "Both",
          FilterRules[
           {
            ImageSize -> cornerPad + resizeAreaSize,
            Sequence @@ rops
            },
           Options@resizeArea
           ]
          ],
        Null
        ],
       Sequence @@ {}
       ],
      If[useEdges,
       Item[#,
          Alignment -> {calr, Top}
          ] &@
        resizeArea[
         {
          imsy,
          Dynamic[tmp1],
          Dynamic[tmp2]
          },
         "Vertical",
         FilterRules[{
           ImageSize -> {imsx, resizeAreaSize[[2]]},
           Sequence @@ rops
           },
          Options@resizeArea
          ]
         ],
       Null
       ],
      If[re,
       If[useCorners,
        Item[#, Alignment -> {Right, Bottom}] &@
         resizeArea[
          {
           imSizeVar,
           Dynamic[tmp1],
           Dynamic[tmp2]
           },
          "Both",
          FilterRules[{
            ImageSize -> cornerPad + resizeAreaSize,
            Sequence @@ rops
            },
           Options@resizeArea
           ]
          ],
        Null
        ],
       Sequence @@ {}
       ]
      },
     Sequence @@ {}
     ],
    {
     If[le,
      Item[#, Alignment -> {Left, catb}] &@
       resizeArea[
        {
         imsx,
         Dynamic[tmp1],
         Dynamic[tmp2]
         },
        "Horizontal",
        FilterRules[{
          ImageSize -> {resizeAreaSize[[1]], imsy},
          Sequence @@ rops
          },
         Options@resizeArea
         ]
        ],
      Sequence @@ {}
      ],
     Item[#, Alignment -> {calr, catb}] &@
      Pane[expr,
       ImageSize -> imSizeVar,
       FilterRules[{ops},
        Options@Pane
        ]
       ],
     If[re,
      If[useEdges,
       Item[#, Alignment -> {Right, catb}] &@
        resizeArea[
         {
          imsx,
          Dynamic[tmp1],
          Dynamic[tmp2]
          },
         "Horizontal",
         FilterRules[{
           ImageSize -> {resizeAreaSize[[1]], imsy},
           Sequence @@ rops
           },
          Options@resizeArea
          ]
         ],
       Null
       ],
      Sequence @@ {}
      ]
     },
    If[be,
     {
      If[le,
       If[useCorners,
        Item[#, Alignment -> {Left, Bottom}] &@
         resizeArea[
          {
           imSizeVar,
           Dynamic[tmp1],
           Dynamic[tmp2]
           },
          "Both",
          FilterRules[{
            ImageSize -> cornerPad + resizeAreaSize,
            Sequence @@ rops
            },
           Options@resizeArea
           ]
          ],
        Null
        ],
       Sequence @@ {}
       ],
      If[useEdges,
       Item[#,
          Alignment -> {calr, Bottom}
          ] &@
        resizeArea[
         {
          imsy,
          Dynamic[tmp1],
          Dynamic[tmp2]
          },
         "Vertical",
         FilterRules[{
           ImageSize -> {imsx, resizeAreaSize[[2]]},
           Sequence @@ rops
           },
          Options@resizeArea
          ]
         ],
       Null
       ],
      If[re,
       If[useCorners,
        Item[#, Alignment -> {Right, Bottom}] &@
         resizeArea[
          {
           imSizeVar,
           Dynamic[tmp1],
           Dynamic[tmp2]
           },
          "Both",
          FilterRules[{
            ImageSize -> cornerPad + resizeAreaSize,
            Sequence @@ rops
            },
           Options@resizeArea
           ]
          ],
        Null
        ],
       Sequence @@ {}
       ]
      },
     Sequence @@ {}
     ]
    },
   FilterRules[{
     Alignment -> {Center, Center},
     ops,
     RowMinHeight -> 0,
     Spacings -> {0, 0},
     Frame -> True,
     FrameStyle ->
      Replace[OptionValue[FrameStyle], {} | Automatic -> GrayLevel[.8]]
     },
    Join[
     Alternatives @@ Keys@Options@Grid,
     Alternatives[RowMinHeight]
     ]
    ]
   ]
  ]

This block of code is actually very simple. Most of it is just duplicated to handle putting resizers on arbitrary edges or corners.

Here's an example of it in action:

doop2

Note that it the FE doesn't automatically respond to a resize in the vertical direction. For that we can explicitly pass an ImageSize, though:

ResizePane[
 Panel[1,
  ImageSize -> Dynamic[x],
  Alignment -> Center
  ],
 Dynamic[x],
 "ResizeAreaSize" -> 2
 ]

doop3

Note that we increased the "ResizeAreaSize" to make it nicer to work with. We could also do something like this to get larger resize grabs without the visual:

ResizePane[
 Panel[1,
  ImageSize -> Dynamic[x],
  Alignment -> Center
  ],
 Dynamic[x],
 "ResizeAreaSize" -> 5,
 "ResizeLocations" -> {Right, Left, Bottom, Top},
 "ResizeAreaOptions" -> {
   Background -> None,
   Frame -> False
   }
 ]

rg4

This can then be resized from any edge or corner.

Finally, I've put all of this code on GitHub here so it may be used like so:

Get["https://github.com/b3m2a1/mathematica-tools/raw/master/ResizePane.wl"];
ResizePane[1]
$\endgroup$
3
$\begingroup$

Related:

I use a resizeable input field in some of my interfaces. So for those who don't find the resize area ugly here is an example of combining the resizeable Pane with another front end widget.

noteField[Dynamic[$notes_, ___], 
   OptionsPattern[ImageSize -> {160, 50}]] := 
  DynamicModule[{imagesizex = OptionValue[ImageSize]},
   Framed[
    Pane[
     EventHandler[
      InputField[Dynamic[$notes], String, Appearance -> None, 
       Background -> RGBColor[1, 1, 0.925], 
       BaseStyle -> {LineIndent -> 0}, FieldHint -> "Enter note here",
        FieldSize -> Automatic, ImageSize -> Dynamic[imagesizex], 
       ImageMargins -> {{0, 2}, {2, 0}}],
      "ReturnKeyDown" :> 
       FrontEndExecute[{NotebookWrite[InputNotebook[], "\n", After]}]],
     AppearanceElements -> {"ResizeArea"},
     ImageMargins -> 0,
     ImageSize -> Dynamic[imagesizex]
     ],
    FrameMargins -> 0,
    FrameStyle -> GrayLevel[0.6],
    ImageMargins -> 0
    ]
   ];

enter image description here

$\endgroup$
  • 1
    $\begingroup$ Nice. And those who do find it ugly can combine the resizers I provide with this to get a text area with resize edges. $\endgroup$ – b3m2a1 Dec 3 '17 at 22:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.