DEV Community

Cover image for Views in Comet
David Ortinau
David Ortinau

Posted on • Edited on

Views in Comet

Comet is a .NET experiment inspired by Flutter and Swift UI for building cross-platform apps with C#.

GitHub logo dotnet / Comet

Comet is an MVU UIToolkit written in C#

All UI in Comet is a View, which makes things really simple, right? Other toolkits may split them into controls, layouts, components, widgets, etc. That makes sense since some are best used for containing and organizing, others best for accepting user input, and others for creating beautiful UI.

Let's take a tour of what's available today in Comet and how you can quickly start composing your UI.

Container Views

The layouts in Comet extend from ContainerView which itself is a View. These managed views arrange and space child views on the screen, and all adapt to whatever size and density the app runs on. The current layout views include:

  • HStack, VStack, ZStack - these views will display children horizontally, vertically, or in z-order
  • Grid, HGrid, VGrid - these views use a powerful column/row system to arrange child views
  • ScrollView - simply contains a single view that scrolls within the visible viewport

The stack family of views are super simple. You add your children inside them, and set some spacing. Here's the basic content of the Comet template:

[Body]
View body()
    => new VStack {
            new Text(count.ToString()),

            new Button("Increment", ()=>{
                count.Value++;
            })              
    };
Enter fullscreen mode Exit fullscreen mode

Each view will accept the minimal set of constructor arguments to be useful. Keeping things simple and efficient is the name of the game. Notice the VStack above doesn't require any constructor arguments. Optionally you can include VStack(HorizontalAlignment alignment = HorizontalAlignment.Center, float? spacing = null).

In C# the children of a view are within the curly braces, comma delimited. The children of our VStack are a Text and a Button. The order is what determines what appears at the top of the screen moving down the screen.

As you can imagine HStack does exactly the same only on the horizontal axis. The only different here will be if you're device is configured for a right-to-left (RTL) region. In that case the content will flow from the right instead of the default which is from the left. For this reason, our alignment options use the terms "Start" and "End" instead.

Now, ZStack is a bit different. The first child in a ZStack will be at the "back" or closest to the parent view surface. The subsequent views will all stack up on top of that view.

[Body]
View body()
    => new ZStack {
            new ShapeView(
                new Circle()
                    .Fill(Colors.OrangeRed)
            ).Frame(width:80,height:80, alignment: Alignment.Center)
            .Background(Colors.Transparent),
            new ShapeView(
                new RoundedRectangle(2)
                    .Stroke(color: Colors.Brown, lineWidth:0)
                    .Fill(Colors.Orange)
            ).Frame(width:30,height:30, alignment: Alignment.Center)
            .Background(Colors.Transparent)
    }.Background(Color.FromHex("#101010"));
Enter fullscreen mode Exit fullscreen mode

Image description

By default children are aligned to the center of ZStack. To change alignment, the children's Frame takes an alignment argument with many options:

TopLeading Top TopTrailing
Leading Center Trailing
BottomLeading Bottom BottomTrailing

In order to move this ZStack as a group around the screen, I can surround it with another view such as Grid and then use the Alignment property to move the stack without disturbing the 2 shapes stacked and centered.

[Body]
View body()
    => new Grid{
        new ZStack {
            new ShapeView(
                new Circle()
                    .Fill(Colors.OrangeRed)
            ).Frame(width:80,height:80)
            .Background(Colors.Transparent),
            new ShapeView(
                new RoundedRectangle(2)
                    .Stroke(color: Colors.Brown, lineWidth:0)
                    .Fill(Colors.Orange)
            ).Frame(width:30,height:30)
            .Background(Colors.Transparent)
        }.Frame(width:80,height:80,alignment:Alignment.Top)
    }.Background(Color.FromHex("#101010"));
Enter fullscreen mode Exit fullscreen mode

The Spacer

One of my favorite parts of layout in Comet is being able to use Spacer to boss around the other elements shoving them into the spacing and position I want. This nifty view can take up a specific amount of space by setting the Frame just like any other view, and by default it will expand to fill the space available.

Look at the example of this simple counter. This will display in a vertical view starting from the top of the screen.

new VStack(){
    new Text(()=> $"Count is {Count}.")
        .Color(Colors.White)
        .FontSize(64)
        .LineBreakMode(LineBreakMode.WordWrap)
        .HorizontalTextAlignment(TextAlignment.Center)
        .Margin(left:30,right:30),
        new Spacer(),
    new Button("Increment", ()=> Count.Value++)
        .Frame(height:76)
        .FontSize(32)
        .Color(Colors.White)
        .Background(Colors.OrangeRed)
        .RoundedBorder(20)
        .Margin(left:30,right:30)
}
Enter fullscreen mode Exit fullscreen mode

If I now add new Spacer() before the Text it will fill the available space and push the content to the bottom of the view:

Image description

If I put a bookend spacer at the end of the VStack, then my content is centered in the view. 😃

Image description

And to complete the story, if I put a spacer in the middle of the elements, they will space evenly. Another way to do this is to use the spacing property on stacks, but I find Spacer to be a powerful way to achieve a variety of results.

Grids

The other major category of layout-base views are the grid family. Anyone with web and css experience has probably use a grid system before, and this may feel quite familiar. Starting with a Grid you define the number of columns and rows you want by providing sizing. Column and row indexing are 0 based, so beware the off-by-one gremlin.

Here is a 3x3 Grid with Buttons across the middle. I'm using the fill methods just to make the spacing and sizing obvious.

new Grid(
    columns: new object[]{ 140, "*", 140},
    rows: new []{"*", "*", "*"},
    defaultRowHeight: 300
    ) {
    new Button().Background(Colors.DarkRed)
        .FillHorizontal()
        .FillVertical()
        .Cell(column:0, row: 1),
    new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical()
        .Cell(column:1, row: 1),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal()
        .Cell(column:2, row: 1),
}.Background(Color.FromArgb("#1d1d1d"));
Enter fullscreen mode Exit fullscreen mode

Image description

The Grid is similar to ZStack in that the z-index (or depth) of the views are determined by their order in the body.

Of course, you can span views across columns and rows as well. Here I'll add another Button to the end of the Grid and span it across three columns:

new Button().Background(Colors.DarkGreen)
        .FillHorizontal()
        .FillVertical()
        .Cell(column:0, row: 0, colSpan:3),
Enter fullscreen mode Exit fullscreen mode

Image description

Now for some power-ups! James Clancey recently added HGrid and VGrid as well as a few helpers. The concept here is that you want to just provide a set of child views and have them displayed uniformly. To do this you can provide the number of cross-axis cells to render, and just add views.

Image description

new VGrid(5) {
    new Button().Background(Colors.DarkRed)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
    new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
        new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
        new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
        new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
        new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
        new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
}.Background(Color.FromArgb("#1d1d1d"));
Enter fullscreen mode Exit fullscreen mode

Oof, despite not needing to declare the size of columns and rows, and not needing to specify the Cell for each view, that's a long block of repetitive code. 😖 Let me make that shorter while adding more content.

new VGrid(5) {
    Enumerable.Range(0,20).Select(x=>
            new Button().Background((x % 2 == 0) ? Colors.DarkRed : Colors.LightSlateGray)
                .FillHorizontal()
                .FillVertical()
    ),
}.Background(Color.FromArgb("#1d1d1d"));
Enter fullscreen mode Exit fullscreen mode

Voilà! You can do the same with Grid, and take advantage of some helpers like NextColumn() (it bumps the view to the next column).

Basic UI Controls

Whether you're building a CRUD app or something more creative, you need a basic set of views to take user input and enable interaction. Comet ships today with:

ActivityIndicator Button CheckBox DatePicker
Image IndicatorView ListView ProgressBar
RadioButton Section SectionedListView SecureField
ShapeView Slider Stepper TabView
Text TextEditor TextField Toggle

Within a ShapeView you can draw shapes: Capsule, Circle, Ellipse, Path, Pill, Rectangle, and RoundedRectangle.

Views support features such as shape clipping, setting borders (stroke), and shadows.

For colors you have your choice of solid paint, linear and radial gradient paint, and more provided by Microsoft.Maui.Graphics.

GitHub logo dotnet / Microsoft.Maui.Graphics

An experimental cross-platform native graphics library.

You've likely picked up on this from the previous examples, as I already know you're an astute developer since you are here with me looking at Comet: to begin customizing a view, Comet provides a fluent syntax that builds upon the base established in the constructor. Chain together your customizations like this:

new Text()
    .Color(Colors.White)
    .FontSize(64)
    .LineBreakMode(LineBreakMode.WordWrap)
    .HorizontalTextAlignment(TextAlignment.Center)
    .Margin(left:30,right:30),
new Button("Increment")
    .Frame(height:76)
    .FontSize(32)
    .Color(Colors.White)
    .Background(Colors.OrangeRed)
    .RoundedBorder(20)
    .Margin(left:30,right:30),
Enter fullscreen mode Exit fullscreen mode

In this example the Text has a foreground color of white, a size of 64, enables word wrapping, centers the text, and spaces the view from the edges of the parent view. I don't need to "read" the Button code to you, however I would like to call your attention to Frame. This gives a view an explicit size, which on most any platform is a great idea to provide if you know the size in order to optimize layout performance.

Random Tips

  1. While hot reloading put all your code in a single file to reduce the need to restart the app.

  2. You can comment out a single line in the chain while editing without breaking the app.

  3. Provide size in Frame if you know it to improve layout performance.

  4. Compose views into reusable functions like my HangulText view in order to create a template for a common style of control.

  5. Have fun and share your experiences on Twitter (I'm @davidortinau), GitHub, or with your 🦔.

Top comments (5)

Collapse
 
codeluggage profile image
Matias

Loving all this Comet content - thank you for showing what's possible in such an approachable and easily digestible way! 🎉

What's your advice for creating UI with Comet that follows standard looks/theming for each platform? Are there best practices for this yet?

Collapse
 
davidortinau profile image
David Ortinau

I'll get into styles/themes in my next post. Right now everything uses the platform native UI, though there's also an option to use drawn UI. The latter gives you a uniform look across platforms.

I haven't mentioned that option yet because I'm not sure it currently works since Comet replaced the original handler implementation with the .NET MAUI core implementation.

Theoretically you should be able to take Microsoft.Maui.Graphics.Controls and initialize those handlers.

github.com/dotnet/Microsoft.Maui.G...

Collapse
 
gpproton profile image
radioActive DROID

The shape feature is quite useful

Collapse
 
sturla profile image
Sturla Þorvaldsson

Does Comet work with Tyzen?

Collapse
 
saint4eva profile image
saint4eva

If .NET Maui supports Tizen, Comet would support Tizen also. And the good thing is, a team at Samsumg is working towards that