cycfi / elements

Elements C++ GUI library
http://cycfi.github.io/elements/
3.06k stars 232 forks source link

tab widget #23

Closed Xeverous closed 4 years ago

Xeverous commented 4 years ago

I would like to implement tab (and some other widgets) but I need some guidance regarding library's code - everything is so composable that I have a feeling I will write duplicated code most of time time.

I have read the post and checked how button is implemented (array composite of 2 elements) but I still don't get many things:

djowel commented 4 years ago

This is not the final API of course. It's just a step towards the direction I had in mind. Feel free to suggest PRs that I can evaluate.

As a matter of fact: before we go to 1.0 is the right time to improve a lot of the APIs, esp. those in the gallery. But keep in mind that the gallery is simply a collection of pre-packaged code that the user can use as-is, or used as basis for their own purposes. That said, I am with you that the gallery should strive to be as generically useful to a broader set of use-cases.

Xeverous commented 4 years ago

116

I have updated the example - you should check how it looks with hnotebook - there is certainly something wrong with tab lengths there but given how consistent are the factories I have made (ideal mirror of v/h and alignment functions) I suspect the issue resides how tab element occupies space that is given to it.

Xeverous commented 4 years ago

There is definitely something wrong with the tab element. When htiled, it does not occupy all the space that is given to it which causes them to look bad because each has a different length. I suspected that exactly the same problem occurs in reverse situation when they are vtiled when they have different heights, so I tried to edit the example and discovered 2 more problems:

diff --git a/examples/notebook/main.cpp b/examples/notebook/main.cpp
index 3e8a509..d549cb9 100755
--- a/examples/notebook/main.cpp
+++ b/examples/notebook/main.cpp
@@ -27,7 +27,7 @@ auto make_tabs(view& view_)
       )
     , tab("Dream")
     , tab("Insanity")
-    , tab("Imagination")
+    , hmin_size(50, tab("Imagination"))
    );
 }
diff --git a/examples/notebook/main.cpp b/examples/notebook/main.cpp
index 3e8a509..7611658 100755
--- a/examples/notebook/main.cpp
+++ b/examples/notebook/main.cpp
@@ -27,7 +27,7 @@ auto make_tabs(view& view_)
       )
     , tab("Dream")
     , tab("Insanity")
-    , tab("Imagination")
+    , tab(hmin_size(50, label("Imagination")))
    );
 }
djowel commented 4 years ago

I really should start using tests, probably starting with layout issues like this. We can't keep on using the examples as tests. I'll think about it...

Xeverous commented 4 years ago

That's a good point but while I can instantly find color/sizing/stretching issues in a given UI I can't imagine how would you write all the test conditions given how complex widget composing can get, even if you implemented a sort of virtual mock view/window class.

djowel commented 4 years ago

That's a good point but while I can instantly find color/sizing/stretching issues in a given UI I can't imagine how would you write all the test conditions given how complex widget composing can get, even if you implemented a sort of virtual mock view/window class.

I know it's difficult. If it was easy, I would have done it already :-) But then again, I think I can start with something simple, like what I did in Artist. Anything is better than nothing.

Xeverous commented 4 years ago

copy-paste mistake:

https://github.com/cycfi/elements/blob/40037b2b49fe23b842ac12e6957e516c7856a01e/lib/include/elements/element/gallery/tab.hpp#L13-L20

And for real issues with the tab:

djowel commented 4 years ago
  • Currently it is a wrapper around button or label widget, it should be more generic like all other widgets in their factories

The tab_element_base class is a gallery element. Of course, you can have different kinds of tabs as long as it follows the basic_choice, or selectable interface.

  • Tab should implement / should help implementing sizing behavior that is correct for a typical notebook: if tabs are htiled, they should have equal height; if tabs are vtiled, they should have equal width.

Not sure what you mean. Pictures will help.

Xeverous commented 4 years ago

The tab_element_base class is a gallery element. Of course, you can have different kinds of tabs as long as it follows the basic_choice, or selectable interface.

And I think the problem lies here. Why the tab_element_base has a std::string member? It's duplicating button/label state. It should only be a behavior-defining widget, not a state widget - if someone wants a tab with text they should do tab(label("string")), if they want a tab with icon they should do tab(icon(...)) and so on. The tab factory function should only add required code to make tab switching work and apply different background colors from the theme depending whether the tab is currently enabled. I'm trying here to follow your library philosophy of reusable elements and no artificial/accidental API limitations.

Xeverous commented 4 years ago

[...] if tabs are htiled, they should have equal height; if tabs are vtiled, they should have equal width

Not sure what you mean. Pictures will help.

I have marked in red space that should be occupied by tab's clickable area. I

tab_widget

Xeverous commented 4 years ago

Another wording: on the left, "Dream" and "Insanity" tabs should be higher, because the third tab has larger height; on the right, the "Dream" and "Insanity" tabs should be wider, because the third tab has larger width.

djowel commented 4 years ago

And I think the problem lies here. Why the tab_element_base has a std::string member? It's duplicating button/label state.

Show me exactly where it is duplicated. When you are claiming things like this, it always helps to be precise.

Xeverous commented 4 years ago

Show me exactly where it is duplicated

https://github.com/cycfi/elements/blob/40037b2b49fe23b842ac12e6957e516c7856a01e/lib/include/elements/element/gallery/tab.hpp#L20-L32

https://github.com/cycfi/elements/blob/40037b2b49fe23b842ac12e6957e516c7856a01e/lib/include/elements/element/misc.hpp#L150-L166

IMO the tab factory function should accept an arbitrary widget and store that widget and only appy necessary behavior for the notebook to function properly.

djowel commented 4 years ago

Another wording: on the left, "Dream" and "Insanity" tabs should be higher, because the third tab has larger height; on the right, the "Dream" and "Insanity" tabs should be wider, because the third tab has larger width.

This looks like the same issue as you had before with tiles. That is how tiles behave. (see layout docs). I'll see if there's a generic solution, or if it is an entirely different problem altogether.

djowel commented 4 years ago

IMO the tab factory function should accept an arbitrary widget and store that widget and only appy necessary behavior for the notebook to function properly.

Still does not make sense. Here's the real base class:

https://github.com/cycfi/elements/blob/notebook/lib/include/elements/element/button.hpp#L215

You want something else that the gallery tab does not give, make one yourself. It is easy, as the tab gallery element shows. The basic_choice is what you want to base your custom tab from. If you want genericity, use basic_choice.

djowel commented 4 years ago

Look, I think you are misunderstanding the purpose of the gallery. It is supposed to be a collection of useful things, as generic as possible, but not try hard to be TOO generic. The generic clases are the basic elements outside the gallery from which the gallery stuff are made from.

There's a balance of ease of use and genericity. That balance should be maintained in the gallery. The key point here is that you can never please everybody and so you make it easy for them to use the gallery stuff as *exemplars to base their own compositions from.

Xeverous commented 4 years ago

Ok, I get what you mean but, given current state of the repository, how much code would I need to make a tab that has something different than text?

I think that elements should supply a factory function that allows to put arbitrary widget inside a notebook tab, this way then user could then do something like this:

vnotebook(
    view,
    pages,
    tab_factory(label("foo")),
    tab_factory(label("bar")),
    tab_factory(image("...")),
    tab_factory(htile(icon_gear, label("settings")))
)
djowel commented 4 years ago

Ok, I get what you mean but, given current state of the repository, how much code would I need to make a tab that has something different than text?

I'll leave that up to you as an exercise ;-) Make your own based on tab_element.

djowel commented 4 years ago

Ok, I get what you mean but, given current state of the repository, how much code would I need to make a tab that has something different than text?

I'll leave that up to you as an exercise ;-) Make your own based on tab_element.

Oh, and the button gallery should also have some interesting examples that you can mix and match.

Xeverous commented 4 years ago

This looks like the same issue as you had before with tiles. That is how tiles behave. (see layout docs). I'll see if there's a generic solution, or if it is an entirely different problem altogether.

I think this time the issue is different.

Previously all widgets were stretchable (buttons, labels, input boxes), but I only wanted input boxes to stretch. It was fixed by adding no_hstretch around button and label columns, which caused them to not stretch which caused input boxes column to take all of the extra horizontal space.

This time there is just 1 row or column of tabs but nothing stretches.

djowel commented 4 years ago

This looks like the same issue as you had before with tiles. That is how tiles behave. (see layout docs). I'll see if there's a generic solution, or if it is an entirely different problem altogether.

I think this time the issue is different.

Previously all widgets were stretchable (buttons, labels, input boxes), but I only wanted input boxes to stretch. It was fixed by adding no_hstretch around button and label columns, which caused them to not stretch which caused input boxes column to take all of the extra horizontal space.

This time there is just 1 row or column of tabs but nothing stretches.

I think they are the same at a fundamental level. What you are describing are effects of this, but I am not sure if I can explain completely, easily.

djowel commented 4 years ago

I think they are the same at a fundamental level. What you are describing are effects of this, but I am not sure if I can explain completely, easily.

Here: http://cycfi.github.io/elements/layout#vertical-tiles

Keypoints:

Xeverous commented 4 years ago

I'll leave that up to you as an exercise ;-) Make your own based on tab_element.

Ok, I will try it and see the code. Maybe then I will turn this into a PR which will express my intent clearer. I understand your points about the balance of ease of use and genericity but I don't get why the simplest "tab with text" is not actually using the label or button elements.

djowel commented 4 years ago

I'll leave that up to you as an exercise ;-) Make your own based on tab_element.

Ok, I will try it and see the code. Maybe then I will turn this into a PR which will express my intent clearer. I understand your points about the balance of ease of use and genericity but I don't get why the simplest "tab with text" is not actually using the label or button elements.

Very valid points! Let's see how it goes. The gallery can evolve and be more generic as needed. The button gallery is an example.

Xeverous commented 4 years ago

If a child element’s maximum limits in either dimension is exceeded, the element will be aligned to the top-left.

This is the root cause then. If I have few widgets tiled vertically:

The problem is, how can I make other widgets to resize to that max horizontal size?


image

On this example there are 2 columns. The column with buttons is intentionally wrapped in no_hstretch so that all of the extra space is given to input boxes. And this works because for magic reason input boxes expand to infinity. But tabs do not, they always keep their minum size. That's my problem - is there any way to make tabs auto-expand whenever there is any extra space?

djowel commented 4 years ago

If a child element’s maximum limits in either dimension is exceeded, the element will be aligned to the top-left.

This is the root cause then. If I have few widgets tiled vertically:

  • the vertical size of the tiling is the sum of their minimum vertical requirements
  • the horizontal size of the tiling is the max horizontal size among them

The problem is, how can I make other widgets to resize to that max horizontal size?

Agreed. I'm thinking about it as we speak. I'll need to investigate the effects if I change this behavior to have elements use more space. The tab-element, and labels for that matter, have fixed size: https://github.com/cycfi/elements/blob/notebook/lib/src/element/gallery/tab.cpp#L47

One way is to wrap it in an align element, but then it will have infinite size and I am not sure that is what you want either. Update: Oh, maybe that's what you want ("any way to make tabs auto-expand whenever there is any extra space")?

Xeverous commented 4 years ago

Update: Oh, maybe that's what you want ("any way to make tabs auto-expand whenever there is any extra space")?

Yes, that's what I want. Input boxes can resize horizontally for obvious reasons, but labels have no point in it as a fixed text has always the same size, so why not add an alignment near them? I wanted to immediately test it with tab(align_center(label("test"))) but then the experiment is blocked by the factory expecting std::string. Looks like I will need to work on the tab factory PR first.

Xeverous commented 4 years ago

image

I'm getting better at it. Now I understand completely how aligning+tiling+stretching combined interact. Expect a PR for the tab interface and a modified notebook example soon.

Xeverous commented 4 years ago

While implementing the PR, I have identified some bugs in proxy:

https://github.com/cycfi/elements/blob/40037b2b49fe23b842ac12e6957e516c7856a01e/lib/include/elements/element/proxy.hpp#L61-L88

Both of these functions take Subject&& subject argument, but:

djowel commented 4 years ago

but labels have no point in it as a fixed text has always the same size

You really should not make strong claims like that without knowing the underlying reasons ;-)

Xeverous commented 4 years ago

So, are there more reasons why labels always have fixed size?

djowel commented 4 years ago

Both of these functions take Subject&& subject argument, but:

  • There is no perfect forwarding because Subject is not deduced. It's already specified by the class template and both the constructor and subject setter are simply using it. Perfect forwarding is only possible when the function template can apply reference collapsing during type deduction.

Correct. Indeed. The ctor should probably take a template to allow convertibility, etc. That is the intent here IIRC.

  • Because of how we use proxy (we always store values of non-reference type), the std::decay is a no-op. It has no purpose when Subject is not deduced and at this point (inside the class) Subject already must be a non-reference type.

The way we use it today does not mean it will be so in the future. There might be a possibility of using references (although we already have the indirect classes), but I am not ready to stifle the interface just yet.

djowel commented 4 years ago

So, are there more reasons why labels always have fixed size?

Good. It's always good to ask first before assuming something is "wrong" :) I suggest you avoid such assumptions without knowing the underlying reasons. Don't assume anything even if it seems obvious to you initially.

Consider this use-case: Imagine this: I want an icon (text icon) at the left of some text with some exact margin. How do I do that if the icon and text have flexible sizes? Have a look at the icon+text buttons, for example:

https://github.com/cycfi/elements/blob/master/lib/include/elements/element/gallery/button.hpp#L85-L105

Can you imagine now how that code will be messed up with the behavior that you propose?

Moreover... Should icons, or images have fixed sized? If not, should they stretch to fit or align to center?

Whatever your answer is, these are design and conceptual questions and there is really no right or wrong answers. It is a choice. IMO, they should be fixed and there should be consistent behavior. After all, there are always align proxies that you can use, and it is easy to add stretch-to-fit proxies if you want some elements to scale fluidly.

That is the underlying design:

  1. Keep the elements as simple and consistent as possible.
  2. Use proxy ("decorators") to change the basic behavior.
Xeverous commented 4 years ago

I managed to workaround current broken forwarding in the tab PR (I will not attempt fixing proxy on this branch, is the job for develop), however I have encountered another bug:

lib/include/elements/element/button.hpp:90:18: error: no match for 'operator=' (operand types are 'std::array<std::shared_ptr<cycfi::elements::element>, 2>::value_type' {aka 'std::shared_ptr<cycfi::elements::element>'} and 'std::shared_ptr<cycfi::elements::tab_element<false, cycfi::elements::label_gen<cycfi::elements::basic_label_base<cycfi::elements::default_label> > > >')
   90 |       (*this)[0] = share(std::forward<W1>(off));

shared pointer has a converting constructor, so I was surprised why it can not upcast the pointed type to base element type. Everything after this error are failed enable_ifs from different operator= overloads which don't tell me much apart from the fact that std::is_assignable said no.

So I went ahead and made a simple experiment:

        tab_element<false, label_gen<basic_label_base<default_label>>>* p1 = nullptr;
        auto p2 = static_cast<element*>(p1);
main.cpp:20:36: error: 'cycfi::elements::element' is an inaccessible base of 'cycfi::elements::tab_element<false, cycfi::elements::label_gen<cycfi::elements::basic_label_base<cycfi::elements::default_label> > >'
   20 |  auto p2 = static_cast<element*>(p1);
      |                                    ^

somewhere across the hierarchy there is a class that uses implicit private inheritance - maybe a bug introduced in recent size refactorings (although there are no such types in this instantiation). Now it's just time for some grep hunting.

Xeverous commented 4 years ago

Surprise, it was actually may fault because I edited the tab_element incorrectly:

lib/include/elements/element/gallery/tab.hpp:17:   class tab_element : proxy<Subject>

Still, I searched the entire repository just in case - not more such mistakes were found.

Xeverous commented 4 years ago

Ok, please build 2070c88e7ca7b26df011b1b430c695e6d096fd88 and look at its result.

It's not a PR but it should clearly express what I had in mind.

Few notable things:

djowel commented 4 years ago

Looks good to me! Now I see what you mean. Yes, this is in line with the design goals of Elements, and I am happy :-)

Xeverous commented 4 years ago

First you educated me how to implement progress bar element correctly to be generic. Now I'm improving the tab because I see a mistake like you pointed me then.

Before I make this change an actual PR, I will try to repair proxy. You can see how the tab currently uses a copy function to workaround (unwanted) rvalue requirement.

djowel commented 4 years ago

First you educated me how to implement progress bar element correctly to be generic. Now I'm improving the tab because I see a mistake like you pointed me then.

Then you educated me to be consistent with the design philosophies :) I'm glad you get it. Sometimes I forget too :-) Thanks for your patience in reminding me :-)

Xeverous commented 4 years ago

122, likely CI problem

Xeverous commented 4 years ago

Since #122 is merged, I think this branch will be ready to be merged onto develop when the copying interface is implemented is removed from the factory.

Xeverous commented 4 years ago

128 brings tab in line with layered button and other button factories:

Xeverous commented 4 years ago

Can this be merged to develop now? The only thing it still needs is the state duplication refactor, just like some buttons.

djowel commented 4 years ago

See branch: https://github.com/cycfi/elements/tree/use-basic_button

Which I branched off "notebook". Check out the changes. This is what I had in mind with the basic_button vs. layered_button, which might become obsolete because that too can be a specialization of basic_button.

Xeverous commented 4 years ago

Great, I will check the diff vs notebook branch.

Xeverous commented 4 years ago

Yes, this is what I had in mind. Go for it on every element that creates duplicate elements in its factory.

I have 2 goals:

And btw, on this branch the notebook example is broken. Tabs can only be switched on once and stay "on" forever.

I'm encouraging you to merge notebook or even use-basic_button branche to develop, as my other work (my own project, bug fixes, theme editor) are depending on more and more bug fixes/elements.

djowel commented 4 years ago

And btw, on this branch the notebook example is broken. Tabs can only be switched on once and stay "on" forever.

Fixed. Simplified traversal code along the way.

I'm encouraging you to merge notebook or even use-basic_button branche to develop, as my other work (my own project, bug fixes, theme editor) are depending on more and more bug fixes/elements.

Merged into develop. Still open to refinements such as aesthetics, API, etc.

Xeverous commented 4 years ago

Great. There is still some small weirdness in the notebook example - the first tab text is highlighted whenever mouse leaves the window.

djowel commented 4 years ago

Great. There is still some small weirdness in the notebook example - the first tab text is highlighted whenever mouse leaves the window.

I'll look into it.

djowel commented 4 years ago

I'll look into it.

Fixed. I'll close this now.