Open emmauss opened 1 month ago
I think it's important to mention that queries sytnax also provides possibility to bring more queries to the table eventually. Like pointer types, aspect-ratio and more.
I would generally vote against UWP-style AdaptiveTriggers (or more broadly - StateTriggers) for reasons of it being alien to Avalonia style system and generally hard to use. Although since we got ControlThemes, StateTriggers could be technically adapted (personally I would just use behaviors instead).
StateTriggers also relatively extendable - something we don't have with our styling at all, like. Avalonia already solved many of these usecases for custom triggers via selectors syntax.
Specifically, on ContainerQuery.
I would say we got an agreement in the team, how this feature should work internally. With mentioned limitations on ContainerType
. By default, Avalonia containers are measured depending on its child size. When children size is changed - parent container is re-evaluated. This is typical for any ContentControl or Border-like control. And actually that's also typical for CSS as well. Which is why they have a very similar Container-Type
property that makes container ignore children measured size.
It's important, as otherwise we have a layout cycle situation, when child adapts to container size, and container adapts to child size.
But higher-level API and usage with styles can be discussed.
Currently opened PR mentions ContainerName
as the way to identify which container should be used. If not specified - closest ContentControl is used with set ContainerType.
As an alternative design, we can have something like this:
<ControlTheme>
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="PART_Container" ContainerType="Width">
<Border x:Name="PART_Child" />
</Border>
</ControlTemplate>
</Setter>
<!-- Container is either a direct .NET reference, or with some help from XAML compiler - part of the template -->
<ContainerQuery Container="PART_Container" Query="min-width:400">
<Style Selector="^ /template/ Border#PART_Container">
<Setter Property="Width" Value="400"/>
</Style>
</ContainerQuery>
</ControlTheme>
This should work well for closed control themes, ensuring that ContainerQuery won't leak or access anything outside of ControlTheme. We even can enforce explicit container for control themes.
This feature also should be well usable outside of ControlTheme's. For example, in the user control where we have a proper namescope set-up:
<UserControl.Styles>
<Style Selector="Border.b">
<Setter Property="Background" Value="Red" />
<Setter Property="Width" Value="200"/>
</Style>
<ContainerQuery Container="MyContainer" Query="min-width:400">
<Style Selector="Border.b">
<Setter Property="Width" Value="400"/>
</Style>
</ContainerQuery>
</UserControl.Styles>
<Grid ColumnDefinitions="*,200">
<ContentControl x:Name="MyContainer" ContainerType="Width">
<StackPanel>
<Border Classes="b"/>
</StackPanel>
</ContentControl>
<Button Grid.Column="1"/>
</Grid>
And I would prefer not allowing ContainerQuery
where name-scope is not available (i.e. styles outside of ControlTheme or control trees).
And while media queries are less powerful than container queries, we might have some discussion on naming in general. If we add other query types, would it be part of MediaQuery class? Or do we want to eventually combine them as a single query? That might or might not depend on the container.
In general I think this is a good idea. However I think for it to really work, Avalonia needs to add relative units based on the container size which can then be applied to controls contained within the container. Much like how CSS has them. It would avoid having to create dozens of very similar container queries to handle various different screen/container sizes.
EG something like this:
<ContainerQuery Query="min-width:400">
<Style Selector="Border.b">
<Setter Property="Width" Value="80cw"/>
</Style>
</ContainerQuery>
Where 80cw
is 80% of the containers width. But I have no idea how easy it would be to implement this sort of thing with how Avalonia's current styling system works.
My questions or opinions:
<Style Selector="@container[width>400 && width < 800] Border.b">
<Setter Property="Width" Value="300" />
</Style>
<Style Selector="@container[width>800 ] Border.b">
<Setter Property="Width" Value="600" />
</Style>
Not quite familiar with CSS, but what's gonna happen if a element has more than one parent that is a container?
In CSS the document hierarchy applies. So the direct parent is generally all that really matters. If the parent itself has a parent then the direct parent rule applies to that. Most of the time, you don't really care though since everything will automatically resize anyway based on the available space as long as you use relative units. But if you only have fixed units then it matters a lot more.
Why not just add an attached property that can enable every element to be container?
CSS sort of actually allows this by explicitly overriding how to treat an element.
https://developer.mozilla.org/en-US/docs/Web/CSS/display
I don't like another dialect of style introduced here, why not just define it as a special selector?
The problem I see with using []
is that it currently refers to a strongly typed property. Here you don't really know what the container is ahead of time. So there would have to be a special case for when the styling system sees @container
that its dealing with something completely different from the normal logic for []
.
My questions or opinions:
1. Not quite familiar with CSS, but what's gonna happen if a element has more than one parent that is a container? Is it isolated by the closes container or becomes a mess? 2. Why not just add an attached property that can enable every element to be container? I think in Avalonia world, panels should be container. 3. I don't like another dialect of style introduced here, why not just define it as a special selector? Example:
<Style Selector="@container[width>400 && width < 800] Border.b"> <Setter Property="Width" Value="300" /> </Style> <Style Selector="@container[width>800 ] Border.b"> <Setter Property="Width" Value="600" /> </Style>
@rabbitism Introducing relative units for control sizes is a great idea, but currently out of the scope of this feature. Could you create an issue for that so we all least have something to track. Thanks
My two cents. I don't see the point of overcomplicating our lives. We already have a great system for managing styles, the Classes, it just needs a few tweaks. First, allow element classes to inherit from their container, allow defining media classes that identify the media. Example of use:
<Window ....>
<Window.Resources>
<Media Name="SmallDesktop">
<Media.Rules>
<ScreenSizeRule Value="640x480"/>
<!-- other rule. Each dev cab be create own -->
</Media.Rules/>
</Media>
<Media Name="HiContrast">
<Media.Rules>
<AmbientLightRule From='.7' To='1'/>
</Media.Rules/>
</Media>
</Window.Resources>
<Window.Styles>
<Style Selector="Border.SmallDesktop">
...
</Style>
<Style Selector="Border.SmallDesktop.HiContrast">
...
</Style>
</Window.Styles>
My two cents. I don't see the point of overcomplicating our lives. We already have a great system for managing styles, the Classes, it just needs a few tweaks. First, allow element classes to inherit from their container, allow defining media classes that identify the media. Example of use:
<Window ....> <Window.Resources> <Media Name="SmallDesktop"> <Media.Rules> <ScreenSizeRule Value="640x480"/> <!-- other rule. Each dev cab be create own --> </Media.Rules/> </Media> <Media Name="HiContrast"> <Media.Rules> <AmbientLightRule From='.7' To='1'/> </Media.Rules/> </Media> </Window.Resources> <Window.Styles> <Style Selector="Border.SmallDesktop"> ... </Style> <Style Selector="Border.SmallDesktop.HiContrast"> ... </Style> </Window.Styles>
Is this only limited to sizing basing on the toplevel? Or based on the parent of the control?
TopLevel because based on its characteristics it constrains all the rest of the UI.
TopLevel because based on its characteristics it constrains all the rest of the UI.
You'll hit cases where you want to make your views modular, but due to controls not knowing how much space they have around them with respect to the toplevel that you have to use multiple size breakpoints to make responsive design. That's why css made container queries. Take a view like this; We can assume this is a dashboard with charts/graphs, summeries in boxes 4 to 10. 1 is the main drawer, and 3 a side bar. Both closable On a 1360 and above screen, you can show this view exactly as it is without changing any styles. At less than 1360, you would want the grids in the main content 2 to have 3 columns instead of 4 At 1270, you'd prefer it be 2 columns. Because it's still in desktop size range, you would not want to auto close the drawer and side bar. At less than 900px, you make the drawer and side bar auto close. The main content has enough space to allow 4 column grids. You also want the blue grid below the green one. At less than 600px, you finally limit the grid columns to 2. If this view was a flat view where all controls are in the main view xaml, using media queries will be enough. But say you want the multiple pages. This view being the main view and the green and blue grids also appearing on their own detail pages. So, you make the green and blue grids custom controls, create detail pages for them, and show them on both the main and detail pages. Now, all the media queries you wrote for the main window will not work for the detail pages. Because we now have new constraints for the green and blue grids. They have more space to allow more columns over a larger size range, but because of the media queries, they will be forced into the columns declared previously. You'd have to write more queries just for the new pages. This is were container queries come into the picture. Because we are only interested in how much space the container is provided by its parent, it will work on any page.
@rabbitism Introducing relative units for control sizes is a great idea, but currently out of the scope of this feature. Could you create an issue for that so we all least have something to track. Thanks
I have created #16962 for discussing relative units.
Why not make MarkupExtensions (or maybe a new variant VisualMarkupExtension) receive the Visual element they are applied to? If we were to have access to the Visual element in the extension, it would be very easy to write customized extensions, which are "sensitive" to other elements in the (logical/visual)tree.
public class RelativeWidth : VisualMarkupExtension
{
Visual element;
string value;
public RelativeSize(Visual element, string value)
{
this.element = element;
this.value = value;
}
public double ProvideValue(IServiceProvider provider)
{
// Omitting all error handling, etc.
return element.Parent.Width * Double.Parse(value);
}
}
(The visual element could of course also be property injected, so that the constructor syntax remains unchanged.)
Usage would be something like this:
<Button Width="{RelativeWidth 0.8}" />
An extension for container-size dependent layout could (for example) be done like this:
<Panel Width="400">
<Button Width="{AdaptiveSize SnapPoints='400:150;800:200;1200:250'}" />
</Panel>
where width is 150 (for container >= 400), 200 (for >= 800), etc. They actual design would probably more involving (query like, '>' '<' etc.), but I hope you get the point.
or even with optional container reference
<Panel x:Name="MyContainer" Width="850">
...
<Panel Width="450">
<Button Width="{AdaptiveSize Points='400:150;800:200;1200:250' RelativeTo='MyContainer'"}" />
...
</Panel>
</Panel>
My point is that such a design would be very versatile and easily extendable (also in user code) and customizable, without the need of changing the XAML compiler for each specific new syntax. It's also in line with existing concepts and syntax in Avalonia. And (from my point of view) can be easy to read as well.
Just a crazy idea ...
If you want to discuss relative units, please do so in https://github.com/AvaloniaUI/Avalonia/issues/16962. This discussion is for container queries. A related, but very different concept that allows for applying different values to use in different situations (EG when the container gets too small, you could switch a horizontal stack panel into a vertical one).
Well, I missed the start of this discussion. Thanks for opening an issue! (a discussion might be better for this type of thing in the future so we can use threads to manage subtopics).
As usual with things like this I have a different viewpoint than the core team. My instinct here is that while ContainerQuery is certainly functional, it's also unnecessary. I'm not sure the original reasoning the CSS working group had to do the things they did though. I also realize there is a goal for MediaQuery and other queries in the future. However, at least in the case of MediaQuery, it doesn't make a lot of sense when the top level size can be queried with something like ContainerQuery. So really, what other queries are needed in the future and how will this provide functionality that couldn't actually be done a different way? What justifies a brand-new concept (based on CSS)?
Anyway, starting from the beginning. I'm basing my viewpoint on two main things: 1) Historically triggers are the fundamental building block for switching styles for things like this. In WinUI they had AdaptiveTriggers as mentioned before. 2) In Avalonia we never got triggers. However, I learned to accept the property value matching syntax of selectors and that turned out to work just fine. It's even preferable in some ways over triggers.
So the logic flows that if style selectors were the fundamental replacement for triggers. And triggers are the fundamental design element in XAML for things like this: why don't we just use style selectors that we already have the concept for?
A third aspect of this is style selectors are appropriately generic whereas ContainerQuery just doesn't seem to fit in well and seems overly specific. It doesn't fall into place with existing ideas and seems like a hack on top of what is already there.
Now what would a style selector look like that could do this? Well, please see my other comments in the PR for even more background:
Long story short though, we need something like this:
Selector="TextBox[#ASpecificParentContainer.Bounds.Width=lt-300]"
This syntax has a few things to talk about: 1) First, it allows specifying a parent in the visual tree by name. This means the selector is pulling information from another control higher up in the tree. We already have an example of this: the nth-child selector although it wasn't integrated quite this far. 2) Second, once we have the parent, we can check properties on it just like we can already do today. 3) The final piece, and the one that doesn't fit quite as nicely, is the less-than syntax. There must be a way to support less-than, greater-than, etc. for this to work. 4) I know there are some reasons for why ContainerQuery has a separate name property. However, I don't think it's necessary -- we simply don't need the functionality as far as I can tell. Multiple selectors can reference the same parent control here using the same name property we use everywhere else. I DO NOT like at all that ContainerQuery is introducing a new name -- that along with the syntax in XAML is one of the biggest design smells to me.
Is this syntax 100% ready? No. But with enough input I'm confident a selector syntax could be decided that was workable for everyone. It would also likely extend the power of selectors across the board so it would be a general-purpose addition to what we already have today.
With API design its very easy to take the easy route and invent (or copy) something new to add on to what you have. Unfortunately, over time that doesn't make a great API. The true skill and benefit to everyone is fully integrating APIs together.
Selector="TextBox[#ASpecificParentContainer.Bounds.Width=lt-300]"
As mentioned before, I really don't think it's a good idea to allow arbitrary comparisons in selector text. I'll go into more detail on this here I guess:
Once we've added a few comparisons, people will start asking for more and before we know it we'll have a turing-complete language in selectors. The goal of the styling/selector system is not to become a separate programming language.
I will note that even CSS (which is generally regarded as being turing-complete) does not have this greater-than/less-than etc selectors.
I don't ever want to see this in a selector:
Selector='#TextBlock.DataContext.Path.To.NestedProperty=='some name' and #TextBlock.DataContext.Path.To.Another.NestedProperty < 1000 and #TextBlock.DataContext.Path.To.Another.NestedProperty > 1000"`
Your example syntax is 90% of the way to this.
Once people start using these expressions, and the expressions start to become more complicated, people will start complaining that performance is bad. And yes, it will be bad unless we start compiling code, at which point we have a fully fledged new language.
This will happen, and there are no debugging tools. We'll then be forced to write these debugging tools.
Selectors were designed to select, and not to query. Even the property match selector's role was to select based on a styled property on itself. Selectors aren't generic. They just have a wide range of uses due to its nature.
CSS introduced containment because websites where becoming more and more complex. An update to an element could require a full layout pass or redraw of the whole DOM. So containment and containers were added, allowing devs to create zones where styles and layout could be applied independently. Added benefit is that styles become more portable, as those zones can be dropped in a webpage without needing to update all existing media queries.
We do not suffer from the layout and redraw issues css suffered from, but we do suffer from lack of responsive styling (what media query aimed to solve) and the fact that most of our UI is made of reusable zones, where media queries would fall short compared to container queries.
Container queries in avalonia doesn't have to only be for sizes, as it is in css. It could, in future, support queries from media queries. So we need a syntax that's easy to read, can host styles that affects a zone so you wouldn't have to create multiple style selectors for each child in the zone, and is easy to extend.
This Selector="TextBox[#ASpecificParentContainer.Bounds.Width=lt-300]"
would not work for a zone. You'd have to write the same query for any child you want to affect.
@grokys @emmauss Thanks as always for the comments and background. I did duplicate some of what was said in the PR here not so much to repeat ourselves but more so everyone could see it. Regardless of the final outcome the discussion is sometimes just as important. Years later, the reasons why things were done are sometimes worth more than what was actually done. Side note: All laws should have a document describing the reason for the law along with it!
@grokys
It's hard to read. Once we've added a few comparisons, people will start asking for more and before we know it we'll have a turing-complete language in selectors. The goal of the styling/selector system is not to become a separate programming language.
We have a strong disagreement here. I think XAML would be far more useful and functional if we could do simple arithmetic and mathematical comparisons. People end up doing all this in code behind or with custom extensions for the most part. It's far more complicated than it needs to be (from a dev/user standpoint). Disagreements are fine and don't need to be resolved.
It's bad for performance
I think you are taking my example to the extreme and then judging the whole based on that extreme. I can abuse anything and make it a performance problem if I want to. That said, a less-than or greater-than comparison is hardly more of a performance impact than an equality comparison. The biggest issue is figuring out what syntax would be readable within the constraints of XML attributes.
Keep in mind my comment in the PR for a full expression syntax was NOT what I'm actually proposing here or what is needed. I was simply saying wouldn't it be awesome to have! (yes, you disagree with this). I think eventually we are just going to need a Razor type syntax to bring all this together.
It's going to cause layout cycles where a selector creates a circular dependency on another
Overall, this criticism seems fair. Without thinking it through fully I suspect most cases in the "switch styles based on container size" wouldn't be an issue though.
@emmauss
Even the property match selector's role was to select based on a styled property on itself. Selectors aren't generic. They just have a wide range of uses due to its nature.
Again, nth-child selector is already bridging the divide here. It's a small jump (from a user standpoint) to generalize this to select based on parent properties. We already violated the rule "selector's role was to select based on a styled property on itself" for good reasons.
containment and containers were added, allowing devs to create zones where styles and layout could be applied independently. Added benefit is that styles become more portable, as those zones can be dropped in a webpage without needing to update all existing media queries.
Yes, that's a nice feature for sure. But it's awful similar in concept to a style group -- nested style -- isn't it? And we already have nested styles in control themes. The parent selector is matched first for all the children. So if we applied that concept here it's an already solved problem. The parent selector could be something like Selector="[#ASpecificParentContainer.Bounds.Width=lt-300]
again extending the selector syntax. Keep in my my point is we have solved similar problems before -- we really need to be sure a different solution is needed now.
Note: It still doesn't sit right with me that we have ControlTheme vs Style and Styles vs Resources collection. Again there were good reasons for it but we aren't integrating like we should be IMO (and what was done in WPF). There are always an infinite number of ways to do things and if we wanted to go a different direction it's always possible. This is just the viewpoint I'm coming from: simpler, generic, re-use of ideas is better.
This Selector="TextBox[#ASpecificParentContainer.Bounds.Width=lt-300]" would not work for a zone. You'd have to write the same query for any child you want to affect.
The idea would be, just like a selector, to target a specific base control or class. TextBox was just an example. Of course something like Selector="Border.sidePaneItem[#ASpecificParentContainer.Bounds.Width=lt-300]
would seem logical to support as well.
Finally, I do have some humility here. I certainly don't know better than @grokys who decided on Selectors to begin with. And I certainly don't know more than the core team who is unanimous here. I'm just pointing out from my viewpoint it seems like selectors should have been extended instead. I'm sure someone in the future might have a similar thought and can come here to see the reasons why things are the way they are. I don't expect you to take the time to follow-up anymore to this thread of the discussion.
@robloo I didn't say selectors must select based on a property of itself. I chose the property match selector because that's one of the non-css selectors we have, and even with that, it selects a group of controls that matches the selector.
Again, nth-child selector is already bridging the divide here. It's a small jump (from a user standpoint) to generalize this to select based on parent properties. We already violated the rule "selector's role was to select based on a styled property on itself" for good reasons.
nth-child is still a css selector. But when a selector requires checking the status of an unrelated control to match, it becomes a query.
Is your feature request related to a problem? Please describe.
With Avalonia supporting browser and mobile platforms, there are very few ways to adapt a view for different screen sizes. Users currently have to design their own responsive controls, use mobile designs for desktop or use different views for desktop, mobile and browser. We require a simple mechanism for adjusting layouts based on the size of the screen, window or the space available to a control.
Describe the solution you'd like
One way to solve this is to create styles that are activated based on the size of a parent or the window. That's where Container Queries come in. Certain controls can behave as container for child controls to query them and activate styles based in the current sizes This feature is similar to css's container queries. By specifying a control as a container, when that control size changes, any style affecting its descendants that's declared in the container query will be activated.
In the above style, if the
Grid
has a width of 600px or more, thus providing the ContentControl with an available space of >= 400, theBorder
with classb
will have a fixed Width of 400px instead of 200px. Containers can not be styled by the styles declared in their ContainerQuery. One main difference between css and Avalonia is the concept of Inline Layouts. In css, inline layout refers to the direction inline elements are laid out. In a normal document, this refers to the Width or horizontal direction. Its inverse is Block, which refers to the direction block elements are laid out. As such, we can't have a 1:1 api implementation of container queries in Avalonia. CSS allows user to declare which direction to allow querying the container's size, and containers sizes can't be affected by children on the direction declared in the ContainerType. Thus I propose the follow enum to match with CSS container type.All suggestions are greatly appreciated.
Describe alternatives you've considered
Media Queries A precursor to Container Queries in css. This allows styles to be activated based on data from the media or device running, such as screen size, orientation, color mode, etc. For the data we are interesting in, which is the size of the device, it is less powerful that container queries. Container queries perform the same function when set on the toplevel, and for complex views, say a screen of charts, knowing the full device size isn't as useful as knowing how much space each chart has to arrange its children using styles.
Adaptive Triggers Adaptive Triggers were implemented in WinUI to perform a similar function to media queries with relation to size. But they are limited to only triggering on Window size changes, and our styling system differs from WinUI's with how less verbose ours is.
Additional context
There's an RFC available that exhibits how this feature can be implemented in Avalonia here https://github.com/AvaloniaUI/Avalonia/pull/16846