Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend the main window #5489

Closed
2 tasks done
nagylzs opened this issue Feb 2, 2025 · 21 comments
Closed
2 tasks done

Extend the main window #5489

nagylzs opened this issue Feb 2, 2025 · 21 comments

Comments

@nagylzs
Copy link

nagylzs commented Feb 2, 2025

Checklist

  • I have searched the issue tracker for open issues that relate to the same feature, before opening a new one.
  • This issue only relates to a single feature. I will open new issues for any other features.

Is your feature request related to a problem?

a := app.New()
w := a.NewWindow(title)
w.SetContent(whatever)
w.ShowAndRun()

The ShowAndRun must be called from the main thread, and it blocks the main thread. I can start another go routine before calling ShowAndRun, and there I would like to wait until the main window appears. Why? Because I need to execute code that needs to know the size of the window, and also because it must be called with a running fyne event loop. But how? Only if I could create a custom main Window, I would be able to execute arbitrary code on Show().

App.NewWindow always returns a fyne.Window, it seems impossible to get notified when the main window is shown or hidden.

I have tried this:

type MainWindow struct {
	fyne.Window
}

func (w *MainWindow) Show() {
	println("I have been shown")
}
// ...
	w := &MainWindow{
		Window: a.NewWindow(title),
	}
// ...
w.ShowAndRun()

What happens is that it simply does not call MainWindow.Show() (tested on Linux).

Interestingly, using callbacks in go is discouraged, but fyne.Window has some callback: SetOnDropped, SetCloseIntercept, SetOnClosed. But there is no SetOnShow, and (apparently) extending the window won't work. (Why? Is it a bug?)

Is it possible to construct a solution with the existing API?

I think it is not possible to construct a direct solution with the existing API.

Describe the solution you'd like to see.

In the above example, MainWindow should be able to extend all of the fyne.Window methods (SetTitle, SetFullScreen, Resize, SetFixedSize, CenterOnScreen, SetPadded, SetIcon etc.) So the requested change is not an API change (or not necessarily). It is simply a request to make it possible to extend fyne.Window.

@dweymouth
Copy link
Contributor

There are callbacks on the app.Lifecycle() API (what you're looking for is SetOnEnteredForeground). Fyne uses callbacks because it is what makes most sense for GUI toolkits where most things happen in response to events.

@nagylzs
Copy link
Author

nagylzs commented Feb 2, 2025

There are callbacks on the app.Lifecycle() API (what you're looking for is SetOnEnteredForeground).

It is true that Lifecycle has SetOnEnteredForeground, but it does not have SetTitle, SetFullScreen, Resize, SetFixedSize, CenterOnScreen, SetPadded, SetIcon etc. The basic issue is that Window cannot be extended. There may be workarounds for some events, but it is not a full solution.

I'm going to formulate the issue in a much more general way below, but it might be something that should be discussed elsewhere, instead of a github issue. Please let me know if there is a better place for discussing it.

Fyne uses callbacks because it is what makes most sense for GUI toolkits where most things happen in response to events.

I wish it was the case, but in many cases fyne uses interfaces that provide the properties, without callbacks. Because of this, they must be extended. It is cumersome and sometimes impossible. For example, fyne.Widget (and fyne.CanvasObject) does not have callbacks like OnResized, OnMoved, OnShown, OnHidden etc, but it does have Size, Position and Visible. In reality, fyne uses a mixture of callbacks and interfaces. It also separates containers and widgets. While containers do have a size, position and visibility just like widgets do, they do not share a common interface that would allow me to respond to size/position changes.

Let's say that I want to create a widget that needs to execute code when another (general) widget changes size or position. If I have an arbitrary wiget (let's say a reference to a fyne.Widget) and I want to be notified when it changes size, position or visiblility, then I'm in trouble.

The idiomatic solution is to extend the widget. My goal is general: respond to resize event of a general widget. It is generalized using an interface (fyne.Widget). But there is no general solution for that. It is because one cannot write general code to extend a widget of an arbitrary type. In order to extend an widget, code must be written to initialize the widget. Calling the original initializer requires a known (fixed) type, which is not available in the general case. Just to give you an example, terminal.Terminal can only be created by calling terminal.New (see fyne-io/terminal#106), and that function can only return a terminal.Terminal. It is similar to what App.NewWindow(title) does: it can only return a fyne.Window, and nothing else. It seems to be impossible to extend fyne.Window and use it to respond to various events. While syntactically it is possible (see my MainWindow example above), it simply does not work.

Developers of custom, general purpose components should have the ability to hide internal structures, and force the end user to initialize the custom component with a custom initializer. I have no problem with that. But it should not prevent the end users from responding to GUI events that are related to the implemented interfaces. The sad fact is that it does prevent that. :-(

What I'm trying to say is that, if there is an interface that is common to a set of widgets, then it should be possible to get notified about events related to that interface, regardless of the concrete implementation. Fyne, and many other GUI toolkits are based on events. It would be essential to be able to respond to events. But it seems to be impossible. For example, fyne.Widget interface does not have callbacks for such notifications. Nor does fyne.Window or fyne.Container. The alternative is to extend a concrete widget, but that is tied to a concrete implementation. It requires writting code for widget initialization, and sometimes it is impossible (either because the inability to initialize private structs, or because it just doesn't work.)

A good solution would have the following characteristics. Given a interface (for example fyne.Widget or fyne.Window):

  1. should be able to respond to all GUI events related the the given interface (e.g. if it has a Size, then should be able to respond to changes of Size)
  2. should be able to do it solely with a reference to an existing widget (instance), regardless of the implementation
  3. preferably, should be able to do it with a few lines of code

At this moment, I think this is impossible to do with fyne. I'm new to fyne, and I might be missing something fundamental.

Without a good solution, it is also impossible to develop new, general purpose components independently that work together. The successful interoperability of general purpose GUI components requires two-way communication between the components, based on their interfaces, using events.

I found a general workaround that -- given a reference to an arbitrary widget -- allows me to respond to resize/show/hide etc. events:

  1. Create a custom wrapper widget with a known (fixed) type, reimplement its resize / show / hide etc. methods, place the event handler code there
  2. Create a new container and layout, set content to that
  3. Add the general (original) widget to the layout

How cumbersome it is:

  • adding a new container is not enough, we also need a layout
  • neither the container nor the layout has an interface that would allow me to respond to resize events, so I have to add a custom widget too
  • in the end, I have to add 4 more layers, and yet I'm still not doing what is intended, just something that works

In the process, lots of new problems are introduced:

  1. adds 4 layers of components
  2. it does not directly respond to the events of the original component
  3. it requires writting lots of extra boilerplate code
  4. it is not general enough, for example code to hide/show the original component should be refactored to hide/show the wrapper

@andydotxyz
Copy link
Member

andydotxyz commented Feb 2, 2025

I think there is a slight misunderstanding of our API design and how widgets work here.

You should not be using the size of a window to do anything with a widget - this is leaking scope and will make the widgets unable to be used in different contexts.

The Layout of a container will decide how to allocate space and widgets will always fill the space they have.

A widget can "respond" to size changing because its render Layout method will be called when it changes.

The cumbersome workaround you describe above sounds like a complicated way to write a custom layout - which is just two methods, MinSize and Layout. https://docs.fyne.io/extend/custom-layout

@andydotxyz
Copy link
Member

If you want to do wrapping without type knowledge you should check out the wrapper types in the fyne-x repo.

@nagylzs
Copy link
Author

nagylzs commented Feb 2, 2025

You should not be using the size of a window to do anything with a widget - this is leaking scope and will make the widgets unable to be used in different contexts.

I'm not sure what a leaking scope is, but I disagree. I can come up with good examples where the Window size is needed, and should be used to do lots of things. For example, what if I want to create a different layout in landscape and in portrait mode? Are you saying that this is something that I should not do? Or maybe I misunderstood.

The issue is more general than tat. I cannot easily write code that responds to size changes of any other widget (not just Window).

The Layout of a container will decide how to allocate space and widgets will always fill the space they have.

Yes, but you are talking about a static interface that has fixed layouts and widgets. Many applications are changing their layouts and widgets on-the-fly, and in most cases, it requires running code for events like hidden, shown, size changed, position changed.

And yes, layouts can lay out widgets, but what if I want to do something that goes beyond that? E.g. change the layout on one part of the app when size changes on another part of the application.

@Jacalz
Copy link
Member

Jacalz commented Feb 2, 2025

Extending the window is not the right solution in any way. However, we should consider expanding the lifecycle API with more callbacks where it makes sense.

@andydotxyz
Copy link
Member

andydotxyz commented Feb 2, 2025

Different layouts for landscape and portrait are possible with layouts. The Layout call is told what space is available and it can switch layout.

You can also use "AdaptiveGrid" which will flow in different directions depending on the window / device orientation.

and in most cases, it requires running code for events like hidden, shown, size changed, position changed.

Yes, layouts handle all of that. We have not coded in the type of "even driven" system you describe because it is not needed with the Fyne model. Everything is self-contained leading to simplicity and great re-usability.

@Jacalz
Copy link
Member

Jacalz commented Feb 2, 2025

I think we should consider implementing something (or a helper around it) like the adaptive layouts that Gnome apps have. I'm a huge fan of how apps more or less become phone apps if they are small enough. However, I don't think the solution for that is having lifecycle APIs for everything.

@dweymouth
Copy link
Contributor

Yeah, the top level component of the window (if it's a custom widget or uses a custom layout) can react to size changes when its Resize function is called. I guess I can see why Andy was experimenting with extending containers, but it's so easy to write a custom widget extending BaseWidget and using NewSimpleRenderer with a container, that IMO we should just update docs to feature this more prominently/with more examples

@andydotxyz
Copy link
Member

andydotxyz commented Feb 3, 2025

I think we should consider implementing something (or a helper around it) like the adaptive layouts that Gnome apps have. I'm a huge fan of how apps more or less become phone apps if they are small enough. However, I don't think the solution for that is having lifecycle APIs for everything.

For sure. It's on my mind but I've still not found an API I like. There's the responsive layout in Fyne-x and a ticket here for "AppSkeleton" that may touch on this. #3044

@nagylzs
Copy link
Author

nagylzs commented Feb 3, 2025

but it's so easy to write a custom widget extending BaseWidget and using NewSimpleRenderer with a container

@dweymouth it sounds easy, but I was never be able to do this

I think fyne is fantastic. No other golang gui library remotely approaches the capabilities of fyne, and I do not want to give up learning it.

However, I have been trying to use fyne for months, and I still cannot create a good mental model for it. I constantly hit "problems" where doing a very simple thing (like responding to an event) requires writting custom widgets and do other "magical" things that are not documented anywhere. :-( Here is an example question from 10 months ago that shows lack of documentation:

https://stackoverflow.com/questions/78137067/fyne-custom-widget-not-refreshed

There I was trying to create a custom widget and I failed, because I could not find documentation on how a WidgetRenderer works. The documentation here https://docs.fyne.io/extend/custom-widget is just an example, and does not explain many key concepts. After 10 months, I still don't know how to write a simple widget that can draw something on the screen. There are no simple examples anywhere on the internet for this (or at least I could not find it). After reading the related documentation, and reading through many articles, I still have a lot of questions just on this topic (custom rendering).

Some weeks ago I tried to extend a widget and I ran into a segmentation fault: fyne-io/terminal#103 (comment) and I still wonder what is wrong. I just can't explain what is happening, the program panics at an unexpected place.

Don't get me wrong, I'm not complaining. There are people who have written entire articles about "why fyne is bad". I am not one of them. I think a lot of work has gone into making fyne so versatile; and I think that if one has the time and skill, should spend energies on improving what has received for free, rather than criticizing. :-)

I only give these examples because I don't know enough to tell what I don't understand. All I know is that "the picture doesn't add up", and when I try to make my own application I always fail, and when I ask others I get the answer "that's not how it should be done".

I have used other GUI toolkits before (wx, gtk and many others). I have been building applications in the last 25 years, and somehow I just don't get it. Maybe the problem is with me, I'm getting old? :-) I think that I have a bad mental model of fyne, and I keep doing things "the wrong way". There seems to be a barrier that prevents me from using fyne efficiently.

@nagylzs
Copy link
Author

nagylzs commented Feb 3, 2025

Different layouts for landscape and portrait are possible with layouts. The Layout call is told what space is available and it can switch layout.

Well maybe, but all layouts built into fyne have a fixed structure. What I mean by fixed is that they lay out widgets that have been added to the layout. But it is not possible to change a layout from Grid to Stack when the aspect ratio of the window changes. In many cases, the structure of the layout needs to be changed when the window goes from landscape to portrait, or when it goes from a small window to a fullscreen window. The built-in layouts cannot be used for this.

So this is another case where I don't know what is the right way to do it.

Things that come into my mind:

  1. Write my own layout. I cannot do it, because there is no official documentation or example code for that. Is there? Is it possible at all? It seems fyne.Layout interface does not have a way to respond to changes in size. It only has two methods: Layout and MinSize.
  2. Write some code that removes the old layout, and creates a new, different layout (with the same widgets) when needed. For example, when width becomes more than height. But this would require me to respond to windows size changes, and that is another thing that I cannot do. :-(

@andydotxyz
Copy link
Member

but it's so easy to write a custom widget extending BaseWidget and using NewSimpleRenderer with a container

@dweymouth it sounds easy, but I was never be able to do this

There are examples everywhere and all through-out our code base and that of the many apps others have made https://apps.fyne.io/. Feel free to open specific questions or post issues, that way we can provide more specific help.

Here is an example question from 10 months ago that shows lack of documentation:

https://stackoverflow.com/questions/78137067/fyne-custom-widget-not-refreshed

Thankfully that question was answered (though I don't think that you've accepted the answer). A custom widget should ExtendBaseWidget and when it wants to update the visuals of the items it uses to render it should update them accordingly.

(as per docs In the Refresh() method it will update the graphical state based on any changes in the underlying widget.Button type. https://docs.fyne.io/extend/custom-widget)

Some weeks ago I tried to extend a widget and I ran into a segmentation fault: fyne-io/terminal#103 (comment) and I still wonder what is wrong. I just can't explain what is happening, the program panics at an unexpected place.

That is a bug in the Terminal code and is being treated as such, the conversation over there seems to be heading to a solution that will work fine. I don't think it's fair to judge the toolkit API design by an external widget.

I have used other GUI toolkits before (wx, gtk and many others). I have been building applications in the last 25 years, and somehow I just don't get it.

Ironically experience with other toolkits (depending on which ones) can hold things back when learning Fyne - it is different to some of them, which is why learning the design is important.

You mostly talk about responding to "events" (which is quite understandable from a GTK+ background) but we don't have them in the same sense. Widgets that want to know about a user action implement the appropriate interface and they will be fed the info. This way fits much better with the Go type system and avoids a "AddEventHandler" and "RemoveEventHandler" for every possible event. In addition it allows platform-specific behaviour extensions without polluting the widget API.

However the ticket topic seems to be about layout - which is quite separate from widgets. A widget fills the space given, it's that simple. To position them in different sizes or positions then update the parent layout or write a custom one as discussed above.

@nagylzs
Copy link
Author

nagylzs commented Feb 3, 2025

I'm not judging. I think I just didn't understand something. Unfortunately, I can't tell you what I don't understand, it might be some general problem in my head. :-(

@nagylzs
Copy link
Author

nagylzs commented Feb 3, 2025

Thankfully that question was answered (though I don't think that you've accepted the answer). A custom widget should ExtendBaseWidget and when it wants to update the visuals of the items it uses to render it should update them accordingly.

Well, it just raised more questions, but I did not write more comments, because I realized that the problem is deeper. Probably with my mental model / thinking.

@andydotxyz
Copy link
Member

andydotxyz commented Feb 3, 2025

Well maybe, but all layouts built into fyne have a fixed structure. What I mean by fixed is that they lay out widgets that have been added to the layout. But it is not possible to change a layout from Grid to Stack when the aspect ratio of the window changes.

It is absolutely possible to do that. The layout is set on a container - and the container will respect a new layout if you set a different algorithm into the container.

Write my own layout. I cannot do it, because there is no official documentation or example code for that. Is there? Is it possible at all? It seems fyne.Layout interface does not have a way to respond to changes in size. It only has two methods: Layout and MinSize.

There is a document on this, https://docs.fyne.io/extend/custom-layout. If it helps you can consider Layout as ContainerHasResized...

But this would require me to respond to windows size changes

No, you should respond to the size of the container not the size of the window.
If you want your app to have different overall layouts depending on window size then put that layout into a container at the top level of your window content. Same outcome. The notes app (https://apps.fyne.io//apps/com.fynelabs.notes.html) and Fysion (https://fysion.app) both do this.

@andydotxyz
Copy link
Member

Thankfully that question was answered (though I don't think that you've accepted the answer). A custom widget should ExtendBaseWidget and when it wants to update the visuals of the items it uses to render it should update them accordingly.

Well, it just raised more questions, but I did not write more comments, because I realized that the problem is deeper. Probably with my mental model / thinking.

If anything it seems like your mental model is of an immediate toolkit, where every draw is a complete refresh. We have a retained mode in which things are always visible and if you want something to look different you apply the change to those visible objects.

@nagylzs
Copy link
Author

nagylzs commented Feb 3, 2025

We have a retained mode in which things are always visible.

My experience is that if I don't add a widget to any container, then it makes it invisible. I already wrote a program that hides a widget by removing it from the container. Is it a bad idea, or is it how I should do it?

Is there a page that lists the possible interfaces (like Tappable) and their applications? I could find some pages for some interfaces, but I would like to know about all "event handling" interfaces. With event based widgets it is easy, I just look at the possible handlers in the source code. For the interface based approach, I cannot get this information from the source code.

@andydotxyz
Copy link
Member

We have a retained mode in which things are always visible.

My experience is that if I don't add a widget to any container, then it makes it invisible. I already wrote a program that hides a widget by removing it from the container. Is it a bad idea, or is it how I should do it?

Apologies perhaps poor wording. Things are visible unless you Hide() them - but you can make them visible again with Show() - it's the same object not a new version requested for the next frame.
A container will not become invisible because it's content was hidden - but a container could be hidden which would impact all of it's child items.
If a widget is not in any container then it has not been "added to the scene" and won't be drawn until you tell it where to go.

Is there a page that lists the possible interfaces (like Tappable) and their applications? I could find some pages for some interfaces, but I would like to know about all "event handling" interfaces. With event based widgets it is easy, I just look at the possible handlers in the source code. For the interface based approach, I cannot get this information from the source code.

If looking at source code is your thing then the easiest way is: https://github.com/fyne-io/fyne/blob/master/canvasobject.go (for platform agnostic abstractions). There are more in the driver/desktop and driver/mobile if you want to dig in to lower level mechanisms.

@nagylzs
Copy link
Author

nagylzs commented Feb 4, 2025

Thank you for your help and for youre time! I'll experiment more, possibly write my own layout before asking more question.

@andydotxyz
Copy link
Member

I think we can probably close this issue, unless you think there is still a problem to be resolved with the docs or API?

@nagylzs nagylzs closed this as completed Feb 4, 2025
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

No branches or pull requests

4 participants