Skip to content

Conversation

@Fwirt
Copy link

@Fwirt Fwirt commented Dec 2, 2025

I had mentioned my intention to write this in the comments on #649, but it ended up being a lot more than the 20 LoC I had projected. I wanted to get this out there before 13 hits release since it seems like something that would be included in a major release. Since @orbitalquark likes to merge changes manually I wrote this so that it wouldn't touch or break any existing functionality, but I suspect in the future it may benefit from being combined with the existing view.size attribute or reimplemented in some other way.

This code extends the view metatable so that Lua code can now reference view.width and view.height to get the dimensions of any Scintilla view, or assign an integer value to view.width and view.height to set the size of any Scintilla view by adjusting the parent splits. These attributes will not resize the window and will raise an error if the resize cannot be completed due to insufficient splits.

It also adds a "resize" event that is emitted when GTK or Qt trigger a widget resize event for a Scintilla view. This can be connected to for the purpose of e.g. "pinning" a split view to a given size so that it doesn't grow when the window is resized or maximized, or for dynamically adjusting tab stops (currently unused function add_tab_stop) to center align or right align text in a buffer.

I can confirm that the changes are tested and working for Qt and curses, but I am unable to build the GTK version on MacOS again, this time due to it complaining about gtk-quartz-2.0 missing (which it isn't). I didn't want to spend time fiddling with CMake again so if someone wants to test the GTK build that would be great.

A snippet to see what the use of this is:

-- Create a single line view below the current one
view.split()
ui.goto_view(-1)
view.h_scroll_bar = false
view.height = view:text_height(1)

-- Add an event handler to "pin" the view to a given size
single_line_view = view
pin_height = view.height
function pin_size(view)
  if (view == single_line_view and view.height ~= pin_height) then
    view.height = pin_height
  end
end
events.connect("resize", pin_size)
-- try resizing the window after this

This is obviously less "correct" than setting a maximum size for the widget in the toolkit but it's more generalizable. You could also use it to do things like update font size based on view width. I actually think that since this implements the general case, it may be possible to eliminate the platform specific command entry handling, since the command entry view is just a Scintilla widget with a label pinned to the front. Just swap the label out for some margin text with the proper styling and it would be visually indistinguishable from what we have now, and you could probably move the focus handling and resizing over to the Lua side of the house so you wouldn't have to maintain separate logic for each platform.

Note that if you run the above snippet, there is some odd behavior (at least with Qt) where if you drag the split handle it will still move, even though the view stays the correct size. If you resize the window or any other splits it will snap back to the correct position. There is probably some update method that needs to be called at the end of set_view_dimension to notify the split to snap its handle to the correct position, but I'm not sure what that would be. It could be some kind of race condition.

Collin Skiles and others added 2 commits December 2, 2025 01:47
Previously it was not possible to set the size of a view that was the
second child of a split to a fixed size because it was not possible
to know the size of its parent. This commit amends the view metatable
to allow Lua to get and set the size of individual Scintilla widgets.
It also adds a "resize" event that is called whenever a Scintilla
widget changes sizes and passes a pointer to the associated view table
to the handler function. This allows Lua user scripts to "pin" a view
to a specific size regardless of window and parent split size changes
by defining a handler function that resets the size of a view on change.
@Fwirt Fwirt changed the title Enhance user code ability to resize views and add an event for view resizing. Enhance ability to resize views from Lua and add an event for view resizing. Dec 2, 2025
@Fwirt Fwirt changed the title Enhance ability to resize views from Lua and add an event for view resizing. Add ability to resize individual views from Lua and add an event for view resizing. Dec 2, 2025
@Fwirt
Copy link
Author

Fwirt commented Dec 2, 2025

I got the GTK build working on MacOS again (had to re-add the target_link_directories(textadept-gtk PRIVATE ${GTK_LIBRARY_DIRS}) directive so the linker would find gtk-quartz-2.0) so I could test it. It's working except for one small bug that I'm having trouble working around: the way that the GTK build handles the command window is to call gtk_widget_hide(command_entry_box), which interestingly makes it stop showing in the Hpaned widget, but does not move the split handle, so the sibling view thinks it's still there. That means that when you set view.height in the GTK build for the lower pane in the root pane, it still allocates space for a one-line command entry instead of taking up the whole window. After monkeying with this for a couple hours I think I've figured out that GTK allocations and panes work together differently than Qt QSplitters (in a worse way). GTK still "allocates" space for a box even when the pane is allowed to shrink and is shrunk down to the minimum size (and hidden). So getting the allocation of the top level pane still gets you a height that's one line short. To demonstrate the problem, try this:

keys['cmd+l'] = function ()
  ui.print(ui.command_entry.height)
end

And watch the output be a non-zero size even when the command entry is hidden. Even more strange, if you expand the command entry box before hiding it, the above print will show the expanded size even though focus_command_entry should set the size to 0 when it hides the box. So far I have tried setting the size request of all the widgets in the command entry box, and the box itself to be 0, I have tried removing the directive to hide the box (just in case for some reason that allowed the widgets to expand), I have changed the getters and setters for the command entry pane to reference the size of command_entry_box instead of command_entry, I've even tried manually setting the allocation for the box in set_command_entry_height, nothing is working to get GTK to shrink the allocation.

It occurs to me that since the view.width and view.height logic is working for views below the top one, refactoring the command window to be a standard view as I mentioned above would solve this issue. But maybe a more experienced set of eyes will figure out what I missed.

@orbitalquark
Copy link
Owner

Thanks for taking the time to put this together. I'll take a look when I have some time and get back to you.

@Fwirt
Copy link
Author

Fwirt commented Dec 3, 2025

Thanks. Right now I'm working on a couple Lua modules that demonstrate the usefulness of this functionality so it has a reason to exist (Scintilla statusbar, drop-in command entry replacement) but once I have a couple working PoCs I intend to circle back around to the UI quirks.

@Fwirt
Copy link
Author

Fwirt commented Dec 5, 2025

I just made a small revision to the way resize events are handled that I haven’t had time to test and commit yet, should be coming later today.

@Fwirt
Copy link
Author

Fwirt commented Dec 9, 2025

phew... It took me way longer than I thought to familiarize myself with all the nuances of programmatically altering text, view focus, and how UI events are emitted, but I finally have a working proof of concept that demonstrates why this is valuable: Presenting the "Textbar" Module

That was stealing all my time from checking to make sure that the build wasn't broken by my changes, now that I have that in a somewhat working state I'll see if I can get those changes pushed tonight so this is ready for review.

I realized that instead of having a wrapper function just to get
access to lua_pushdoc, I could add a couple of fake "types" for
emit() to pick up on, so that we can just directly push SciObject
and document pointers directly onto the stack from emit. I feel like
this simplifies things. There's also some code in here where I was
goofing around with trying to get GTK to allow me to resize the
command entry to less than 15 pixels tall, but so far nothing is
working...
@Fwirt
Copy link
Author

Fwirt commented Dec 9, 2025

Alright @orbitalquark, I think this is finally ready to review now. I'm sure you'll have questions and want to refactor this in your own style, but I think the addition of view.width, view.height, and events.RESIZE make some interesting functionality possible. It's working (more or less) on all 3 platforms.

@orbitalquark
Copy link
Owner

Let's suppose view.width = 200 would set the minimum width for a view to 200. Can we get GTK and Qt to automatically change splitter positions such that the given view is that minimum dimension? Or do we still need to iterate over those splitters and manually set their positions? I'm just wondering if we can simplify at least the GUI part to leverage their respective toolkits more.

By the way, we shouldn't define new LUA_TBUFFER and LUA_TVIEW types -- we should use the existing LUA_TTABLE and (lua_pushdoc(...), luaL_ref(...)) or (lua_pushview(...), luaL_ref(...) mechanism.

@Fwirt
Copy link
Author

Fwirt commented Dec 12, 2025

Let's suppose view.width = 200 would set the minimum width for a view to 200. Can we get GTK and Qt to automatically change splitter positions such that the given view is that minimum dimension? Or do we still need to iterate over those splitters and manually set their positions? I'm just wondering if we can simplify at least the GUI part to leverage their respective toolkits more.

I'm learning both GTK and Qt as I go, so I'm not 100% sure of the answer here, but from my research both Paned and QSplitter objects are designed to be user resizable, rather than app managed. Where a box layout would automatically resize to the minimum widget size, my understanding is that the split widgets leave size up to the user, requiring us to resize manually from user code. Setting the minimum size will just prevent the splitter from making the window any smaller, which is not necessarily what we want if we want to preserve user control

By the way, we shouldn't define new LUA_TBUFFER and LUA_TVIEW types -- we should use the existing LUA_TTABLE and (lua_pushdoc(...), luaL_ref(...)) or (lua_pushview(...), luaL_ref(...) mechanism.

I did that in my initial commit, but the problem is that lua_pushdoc and REGISTRYINDEX aren't available to the platforms, so I had to implement a wrapper function that just called emit with the document pointer in textadept.c. It seemed a little clunky, but if that's the preferred approach then that's fine too.

@orbitalquark
Copy link
Owner

Setting the minimum size will just prevent the splitter from making the window any smaller, which is not necessarily what we want if we want to preserve user control

I see. Usually with widget sizers, we can have some widgets as shrinkable, and others as growable. If we say the shrinkable one has a min width of 200, then the growable one should auto-size until it's as large as can be (the shrinkable one is 200). And by auto-size, I mean we'd probably call some sort of sizer function that recomputes child geometries. It's something to keep in mind, but you may very well be right that splitters are user-controlled.

By the way, we shouldn't define new LUA_TBUFFER and LUA_TVIEW types -- we should use the existing LUA_TTABLE and (lua_pushdoc(...), luaL_ref(...)) or (lua_pushview(...), luaL_ref(...) mechanism.

I did that in my initial commit, but the problem is that lua_pushdoc and REGISTRYINDEX aren't available to the platforms, so I had to implement a wrapper function that just called emit with the document pointer in textadept.c. It seemed a little clunky, but if that's the preferred approach then that's fine too.

We should probably declare something in src/textadept.h like void view_resized(SciObject *view);, have the platforms invoke that function, and have src/textadept.c handle the details (i.e. emit the event).

@Fwirt
Copy link
Author

Fwirt commented Dec 13, 2025

That's exactly what I had in my initial attempt #8089db4, so if that strategy seems better to you then that's fine. I think having the "fake" types in src/textadept.h might save a few lines of code but I didn't actually check. It's your call. There are a bunch of ways this could be implemented, I think I was aiming for backwards compatibility.

@orbitalquark
Copy link
Owner

Ah, I see that now. Yes, that is the preferred approach.

@orbitalquark
Copy link
Owner

Thanks again for submitting this. I've had a look at this and I have some concerns (and sorry in advance if I missed something in your previous set of comments):

  • If we go with this, then we have view.width, view.height = ... and view.size, view.parent_size = .... I generally don't like to have this kind of duplication. Now, you cannot exactly accomplish arbitrary sizing using view.size and view.parent_size because we cannot compute the width or height of a split widget. However, if I have ui.get_split_table() return a widget's width, height, and split position, then I think we have enough information to set an arbitrary width and height. This means view.width, view.height = ... becomes a shortcut that can be turned into a Lua view:resize(width, height)-kind of function.
  • I am worried about a view.width = ... -> events.emit(events.RESIZE, ...) -> view.width = ... -> events.emit(events.RESIZE, ...) recursive loop, for example if a width cannot be set precisely, but the event handler expects that it can be done. I'd much rather have the user execute a "relayout" type of command that will perform resizing manually instead of automatically.
  • I honestly don't see a good reason to allow the command entry height to be less than the height of a single line.
  • The set_view_dimensions() feels like a form of duplication over all three platforms that can be implemented just once in Lua, based on my first bullet point.
  • As we discussed elsewhere, the terminal platform as written will exceed my artificial LOC limit.

I'm going to propose the following:

  1. I change ui.get_split_table() to return a split widget's width and height in addition to its split position.
  2. That, coupled with the existing view.size and view.parent_size should enable one to create a module or something similar that can resize views with more control than is currently possible.
  3. I'm going to rename view.size and view.parent_size to view.split_pos and view.parent_split_pos, respectively, to better indicate their function.

I do not plan on creating a module that implements something like view:resize(width, height) myself, but it should be possible with the new information.

If you have any thoughts about this, let me know. We can always iterate to try and get to something satisfactory.

@Fwirt
Copy link
Author

Fwirt commented Dec 17, 2025

I really appreciate you taking the time to look at this and consider it.

  • If we go with this, then we have view.width, view.height = ... and view.size, view.parent_size = .... I generally don't like to have this kind of duplication. Now, you cannot exactly accomplish arbitrary sizing using view.size and view.parent_size because we cannot compute the width or height of a split widget. However, if I have ui.get_split_table() return a widget's width, height, and split position, then I think we have enough information to set an arbitrary width and height. This means view.width, view.height = ... becomes a shortcut that can be turned into a Lua view:resize(width, height)-kind of function.

That makes sense, my only concern is the overhead of calling ui.get_split_table() repeatedly but I suppose a small tree traversal isn't too egregious.

  • I am worried about a view.width = ... -> events.emit(events.RESIZE, ...) -> view.width = ... -> events.emit(events.RESIZE, ...) recursive loop, for example if a width cannot be set precisely, but the event handler expects that it can be done. I'd much rather have the user execute a "relayout" type of command that will perform resizing manually instead of automatically.

To be honest, there are plenty of ways to easily crash Textadept with the event handlers we have now. I have triggered a number of stack overflows by connecting a handler to UPDATE_UI that indirectly emits another UPDATE_UI by changing something that I didn't expect. I have locked myself out of the interface by adding a KEYPRESS handler that throws errors. I've locked myself out of entering commands by accidentally stealing focus while the command_entry is open. So I'm not sure how much of an issue a misbehaved event handler is since it will probably only be accessed by power users. But I could definitely see two views fighting over the split position and locking up the interface if it isn't used with discretion.

That being said, I wasn't completely satisfied with how setting view.size in the handler worked, it tended to trigger odd behavior with the toolkits. I feel like some kind of event is needed because right now there is no good way to tell if anything (including the window) has been resized other than polling. And it's annoying to have to press a key combo to resize everything whenever you adjust the size of a parent view, split a parent view, or resize the window. But it's not a huge issue.

  • I honestly don't see a good reason to allow the command entry height to be less than the height of a single line.

Fair enough, I mostly did that because I couldn't get GTK to behave, and I was playing with QSplitter collapse behavior.

  • The set_view_dimensions() feels like a form of duplication over all three platforms that can be implemented just once in Lua, based on my first bullet point.

Agreed.

  • As we discussed elsewhere, the terminal platform as written will exceed my artificial LOC limit.

Yeah, I didn't realize that until after I'd proposed the bigger restructure of panes/views. I can see how adding any more features at this point poses a challenge.

I'm going to propose the following:

  1. I change ui.get_split_table() to return a split widget's width and height in addition to its split position.

  2. That, coupled with the existing view.size and view.parent_size should enable one to create a module or something similar that can resize views with more control than is currently possible.

I think that will be sufficient. As I mentioned in another thread, I found a way to hack around fetching size (move the split beyond its maximum position and then query the value) but I don't know if it's good practice to rely on that and it could cause ugly flickering if you're constantly flipping sizes.

  1. I'm going to rename view.size and view.parent_size to view.split_pos and view.parent_split_pos, respectively, to better indicate their function.

I really like this, even if nothing else comes of this I think size is a misleading field name and is what triggered me to investigate this in the first place. I know it breaks backwards compatibility but at least you're coming up on a major version number here.

I do not plan on creating a module that implements something like view:resize(width, height) myself, but it should be possible with the new information.

I think that's fair to keep the core lean.

I know it takes effort to review these changes so I appreciate the thought put into this. I've given my feedback above, I'll respect whatever you decide.

@orbitalquark
Copy link
Owner

Fair point about event shenanigans. If we have a view resize event, we should probably have a window resize event too. Or at least have one event that encompasses both. I will look into this.

@orbitalquark
Copy link
Owner

I was experimenting with a resize event, and GTK is giving me trouble. When I use the command entry to run events.connect('resize',function(v)print('resize',v)end), and then resize the window a little bit, eventually I'll trigger an endless loop and the output buffer will print endlessly. I can click on the "Untitled" tab a few times to eventually stop it, or some more resizing might do it. Either way, this is a concern.

@Fwirt
Copy link
Author

Fwirt commented Dec 21, 2025

I was experimenting with a resize event, and GTK is giving me trouble. When I use the command entry to run events.connect('resize',function(v)print('resize',v)end), and then resize the window a little bit, eventually I'll trigger an endless loop and the output buffer will print endlessly. I can click on the "Untitled" tab a few times to eventually stop it, or some more resizing might do it. Either way, this is a concern.

I think I ran into this when I was testing the GTK build, but I was hoping it was just a "GTK is kind of broken on MacOS" issue. I wonder what the correct incantation is to prevent GTK's event loop from getting stuck.

I know you've already spent a lot of time on this and I don't want it to hold up 13.0. It seems like renaming the size field and adding width and height attributes to the split table gets us 90% of the way there, and adding the events could be something for a minor release since that won't be a breaking change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants