Terminal.Gui
Terminal.Gui copied to clipboard
Add mouse/keyboard resizing of views
Right now, dragging is handled w/in TopLevel. This needs to change to be handled within Border (or in Application, but via an interface with Border).
- [x] Define
View.Movable- if true and view has a top Border, it can be dragged - [ ] Define View.Sizable - if true and a view has left/right/bottom Border it can be resized
- [ ] Determine keyboard drag/size UI
- [ ] Remove temporary hack in
Application.cs:
Related:
- #https://github.com/gui-cs/Terminal.Gui/issues/2502.
My proposal:
Tiled Mode:
Mouse:
- Dragging
Bordertop or bottom resizes the view vertically - Dragging
Borderleft or right resizes the view horizontally
Keyboard:
- Hit Ctrl-F10 to enable drag/resize mode
- A line will get a
◊indicating it's active for resizing. Left/Right/Up/Down will size. - Tab to move to next line
- Shift-Tab to move back
- Ctrl-F10 to exit drag/resize mode
Overlapped mode:
Mouse:
- Dragging
Bordertop moves the view - Dragging
Borderbottom resizes the view vertically - Dragging
Borderleft or right resizes the view horizontally
Keyboard:
- Hit Ctrl-F10 to enable drag/resize mode
- A line will get a
◊indicating it's active for resizing.- If the selected line is
Bordertop, then Left/Right/Up/Down will move the view. - If the selected line is
Borderbottom, left, or right, then Left/Right/Up/Down will size.
- If the selected line is
- Tab to move to next line
- Shift-Tab to move back
- Ctrl-F10 to exit drag/resize mode
This needs to fix #2536 too
Based on @Nutzzz's excellent SpinnerView enhancments, I propose we use SpinnerView to show which border has focus for sizing instead of the diamond that @tznind originally used for TileView.
E.g. This one for horizontal:

and this one for vertical:

This will require LineCanvas to support hosting a SpinnerView as a point (where .Length == 0) I think.
Y'know, I like the idea in general, but I'm personally not thrilled about those spinners in particular. I feel like one of these might be better (if not perfect).
[What do you think, 2-frame or 3-frame?]
2 frames, horizontal and vertical:
3 frames, horizontal and vertical:

@BDisp this is another Issue related to
- #2994
If one of the goals of this feature is to support TGD resizing use cases then I think that the solution needs to be decoupled from Borders.
It must be possible to drag and resize any view (regardless of whether it has a Border). For example:
It also needs to be possible to drag multiple views at once e.g.
If one of the goals of this feature is to support TGD resizing use cases then I think that the solution needs to be decoupled from Borders.
It must be possible to drag and resize any view (regardless of whether it has a Border). For example:
It also needs to be possible to drag multiple views at once e.g.
I'll start thinking about this...
I can't see anything really challenging, but it will require some thought...
Are you interested in collaborating with me on it?
Are you interested in collaborating with me on it?
Definetly! lets see what we can do.
Drag
Drag Phases (i.e. State)
In order to support Undo and other stuff (like cancelling an ongoing drag) I think we should keep state in the same way TGD does with DragOperation and it's DragMemento:
- Only one drag operation can be running at once
- Use
ContinueDragstyle
So the process is:
// Establishes the origin from which all vectors are measured
StartDrag (x,y)
// Updates positions of Views, each vector replaces the last (i.e. they are not additive)
ContinueDrag(new Vector(2,5))
ContinueDrag(new Vector(3,5))
ContinueDrag(new Vector(4,5))
// View is moved to x+4 y+5 and drag state is cleared
EndDrag()
Drag Multiple At Once
TGD lets you select multiple views with a selection box. I think this can be left in TGD. But we do need the ability to drag multiple views at once.
For sanity sake probably we should require that all views in a multiple drag should be in the same parent View.
Drag Boundary
When a drag results in some/all of a view spilling out of it's parent:
/// <summary>
/// True to allow dragging a view such that part/all of it is
/// outside of its parent View's viewport
/// </summary>
bool AllowDragOffscreen;
Drag Initiation
There are 3 options for a View when it comes to initiating a drag
enum Dragability
{
/// <summary>
/// View cannot be dragged
/// </summary>
NotDraggable,
/// <summary>
/// View can be dragged by clicking and dragging at row 0 of
/// the view.
/// </summary>
DragFromTop,
/// <summary>
/// The view can be dragged by clicking and dragging anywhere
/// in the view
/// </summary>
DragFromAnywhere
}
Pos Types
When dragging we need to decide which Pos types can be moved and which cannot.
For example:
- PosAbsolute (yes)
- PosPercent (yes but be careful to translate new coordinates to keep as Percent)
- PosFunc (no)
- PosView (maybe)
Dragging a view with another that is PosView connected to it (e.g. Left of) needs considered also. Especially if dragging to another view.
Drag into another View
This is an opt in feature. It could be a global value or View specific. But if it is tied to Views then it makes multiple view drags more complicated.
bool AllowDragToOtherViews;
To support dragging a view into another view we must think:
- Does it make sense to drag a view into a Button (probably not)
- Does it make sense to drag a view into a FrameView (yes)
So we need some kind of confirmation probably an event with a Cancel member is easiest
event EventHandler<DragEventArgs> StartDrag;
event EventHandler<DragEventArgs> EndDrag;
class DragEventArgs
{
/// <summary>
/// Describes the views being moved and where they are being moved to.
/// Note that you can use set to change which views are dragged e.g. to drag multiple
/// </summary>
public DragOperation DragOperation { get; set; }
/// <summary>
/// Set to true to abandon the <see cref="DragOperation"/> and return
/// all views to original positions
/// </summary>
public bool Cancel { get; set; }
}
Composite views
Some views have sub views that complicate click and drag. For example TabView. We need something more structured than the current hacky implementation I have:
/// <summary>
/// <para>
/// Sometimes <see cref="View.FindDeepestView"/> returns what the library considers
/// the clicked View rather than what the user would expect. For example clicking in
/// the <see cref="Border"/> area of a <see cref="TabView"/>.
/// </para>
/// <para>This works out what the real view is.</para>
/// </summary>
/// <param name="hit"></param>
/// <returns></returns>
public static View? UnpackHitView(this View? hit)
{
if (hit != null && hit.GetType().Name.Equals("TabRowView"))
{
hit = hit.SuperView;
}
// Translate clicks in the border as the real View being clicked
if (hit is Border b)
{
hit = b.Parent;
}
// TabView nesting of 'fake' views goes:
// TabView
// - TabViewRow
// - View (pane)
// - Border (note you need Parent not SuperView to find Border parent)
if (hit?.SuperView is TabView tv)
{
hit = tv;
}
return hit;
}
A few years ago I was interested in implementing mouse/keyboard drag and drop and resizing. Then I lost the will to continue.
Indeed, @tznind is right in stating that some of the features only make sense in TGD, otherwise any user could create another IDE very easily if TG already provides it. For example, it doesn't make sense that a TextField can be resized or moved in TG, but only in TGD. Drag from FileDialog to a TextView is valid in TG to open a file but in TGD it is moving the FileDialog to the limits of the TextView. Drag from ListView over the selected item to a TextField is valid in TG, and a ContextMenu can be opened to choose to copy or move the item, but in TGD it is moving the ListView to the limits of the TextField, or possibly linking the TextField.Text with the selected item.
In fact, the resizing and movement of a view must be decoupled from the border and only be coupled to the limits of the frame for resizing and within the limits of the frame for movement. In TG, only views that need to be resized and movable, their code must be overwritten in the border. In TGD the behavior will be normal, activating the necessary properties to be able to use them.
In TG, by default, a view cannot be resizable or movable, but only in some derived classes, such as Window and Dialog, if they are configured for that purpose. If it is resizable then a border must exist in order to select the appropriate glyph on either side of its limits. If it is simultaneously movable, in addition to the border, the thickness of the top of the border must be at least two, which will allow the view to be moved if the mouse is placed from the second thickness.
I'm glad this is being debated and will be very relevant for the advancement of this resource.
Good stuff.
Not a lot of thoughts so far. But one...
Add a flag to Arrangement:
- DesignMode
When set, effectively hides the Viewport from the mouse hit test code. Thus press and drag anywhere from Border-in works. Even if Border thickness is 0.
Also, look at Highlight/HighlightMode. Can be extended to be combined with Arrangement.DesignMode to enable the whole view to be highlighted.
But it would be useful if Arrangement.DesignMode was only visible internally, therefore only to TG and TGD.
But it would be useful if
Arrangement.DesignModewas only visible internally, therefore only to TG and TGD.
Why would we not want someone else to build a design-time tool like TGD?
Why would we not want someone else to build a design-time tool like TGD?
I thought you wanted TGD exclusivity that belongs to gui-cs. If that's not the case then I take back what I said.
It sucks so much for us that every platform and use tha forces us to have to have a "driver" to jump through different hoops to make this all happen. 😤
It's things like this that make libraries like TG so damn nice to use - abstracting away all the BS so the consumer can have a more consistent experience without worrying about it.
/gripe 😆