Custom UI Elements with Eto.Forms

Hey everyone,

@sonderskovmathias asked me to share some experience with building custom UI elements with Eto.Forms. Instead of spamming the original thread, let’s start a dedicated conversation here.

Here is a sneak peek at our upcoming Rhino plugin called Shape. It makes terrain modelling fun, interactive, and super productive! But that’s a separate story…

In the process of making it, @Wiley and I have learned a lot. Everything you see above are custom UI elements written on top of Eto.Forms.Drawable (side panel) or hacks deep into the DisplayPipeline (HUD elements).

Feel free to ask specific questions and we’ll try to answer to the best of our abilities.

I also wanted to ask @bobmcneel if there are any plans of increasing McNeel’s involvement in the Eto project. Obviously, @curtisw is putting a lot of effort into further developing the repo but it has grown beyond what one person can reasonably maintain. I believe @dale and @stevebaer are doing work on the UI for the WIP version but these contributions don’t seem to find their way into the open-source codebase. Since Eto is the officially recommended UI framework for cross-platform Rhino plugin development, it would be nice to understand McNeel’s commitment level to the project. As it is now, Rhino is not even listed as an app using the framework:

Tagging a few people, who expressed interest in custom UIs in the past.
@DavidRutten, @AndersDeleuran, @aramon, @lando.schumpich, @mlukasz87, @matteo1, @Felipe_Penagos, @maxsoder, @Holo, @johannavarro.art, @D-W, @jordanmathers.jm

36 Likes

Tagging more people:
@agi.andre, @RdK, @kitjmv, @Gijs, @Michael_H, @benjamin.moulton, @luc.nadibaidze, @johannavarro.art, @zhuangjia777, @wim, @Philip3, @felixmariotto, @Czaja, @rowen1124

2 Likes

Looks great! The responsiveness feels really good.

I’m interested in learning more about the radial brush HUD element as I would like to add a radial/contextual menu to my own workflow that fans out icons, with each icon triggering a specific code function.

Is it possible to add elements to the viewport via Eto or is all of that in the video a custom display hack?

Thanks for sharing all this!

It is definitely possible, in fact we already have a similar HUD widget with a context-sensitive radial menu:

No Eto involved in this one, just listeners to mouse/keyboard events and drawing shapes via the DisplayPipeline.

8 Likes

Yes, that’s it!

I imagine using Middle Mouse as the mouse down event to activate the menu and then Middle Mouse Release while hovering over top a menu item treats it as a “click” so that it can be super fast to use.

The grasshopper canvas radial menu in GH1 is similar to this and allows you to release over a menu item or manually click it.

Sounds doable. Our HUD menu pops up on ALT key press and users need to specifically click on the pie slice to activate the command.

One thing to keep in mind are existing interaction patterns and muscle memory. My Rhino is set up to use the MMB press to pan the viewport. The last thing you want, is for your custom UI to confuse/annoy users. Ideally, all these interactions would be user-customizable in the plugin settings.

1 Like

Good point. Agreed on being able to customize the shortcuts as everyone works different with various preferences.

Is there any documentation that helped you with the radial menu/viewport display drawing that you could point me towards so that I can learn and try to build something similar?

Thanks for the info!

I’d start with creating basic Mouse/Keyboard events and drawing custom geometry in DrawOverlay

Rhino.Display.DisplayPipeline.DrawOverlay += DisplayPipelineEvents.DrawOverlay;

public static void DrawOverlay(object sender, DrawEventArgs e)
{
if (Display.OnScreenWidget != null)
    {
    Display.OnScreenWidget.OnDrawOverlay(e);
    }
}

public void OnDrawOverlay(DrawEventArgs e)
{
// your drawing code goes here
e.Display.Draw...
}
2 Likes

Thank you @mrhe !

oh wow, this is great! it’s amazing how performance behaves.

Hey @mrhe, thanks for the question, and hopefully I can give you some insight. Eto.Forms is the officially recommended UI framework since it is what we use internally for Rhino’s interface and is known to work great on both platforms. Most of the initial work in Rhino 6-7 using Eto was done to bring Mac up to speed to the Windows counterpart, with the intention that the Eto version of the UI would eventually replace the aging MFC bits on Windows. It was also used to implement any new UI so that it could be shared between platforms. With Rhino 8, you’ll see much more of the UI using Eto as we have removed a lot more of the Windows (and Mac) specific code.

All of the features/enhancements that have been necessary for Rhino 8 have been put into Eto. The things that are not in Eto directly are our new custom controls (using Drawable just like you have), the docking system (which is very rhino specific in its implementation), and the custom theme we have applied to WPF to make things look much better by default when using standard Eto UI (which anyone can do for their own Eto UI on Windows, or use Rhino 8’s theme).

One advantage to using Drawable is it will look the same on either platform. With Rhino we have opted for a blend of using standard controls and custom controls so that Rhino will feel natural on both platforms while also giving us more advanced controls to give a better user experience.

As for increasing involvement, I’m not sure what that would entail. What further can McNeel contribute other than improving upon the framework as it is currently? Yes most/all of that goes through me, but it has certainly not been a bottleneck over the years imo.

6 Likes

Thanks for a comprehensive answer @curtisw!

As for increasing involvement, I’m not sure what that would entail. What further can McNeel contribute other than improving upon the framework as it is currently?

As mentioned in the original post, I totally appreciate all the hard work that you’ve put into creating and maintaining Eto. I was just wondering whether the custom controls developed for Rhino will ever be shared with the community. Be it part of the official repo, or as code snippets along other developer samples. It would be great to study, learn from, and potentially reuse @DavidRutten’s work for GH2, your and @Dale’s custom panels, gradient controls etc.

Currently, there is no obvious way for developers to leverage these controls and build on top of them. McNeel has a great history of open sourcing significant parts of the codebase and imho the UI part seems to be a very good candidate to go all in. Especially, given the heated debates on the forums each time a change is introduced.

4 Likes

Id love to see some samples on how to leverage rhino8 theme and also how to add templates to to Eto. Is it exactly like with wpf?

You have a very cool and beautiful look. I could only make a custom Button and Toggle.(just change the picture when modifying the Toggle.) already tested on Mac OS. everything works. :sweat_smile: I’m a long way from you.

Hey @sonderskovmathias, there is a new extension API in Rhino 8 WIP, Rhino.UI.EtoExtensions.UseRhinoStyle(this Control control). Use it like this: myForm.UseRhinoStyle()

This applies the WPF styles we have created for Rhino 8 on Windows to support light/dark mode and the ability to change the UI colors in advanced settings. It does nothing on Mac (currently) as the style is standard for non-custom controls.

Applying WPF templates to Eto can be done by following the link I provided. A minimal example of what we do in Rhino is like this for WPF’s Button:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:eto="clr-namespace:Eto.Wpf.Forms.Controls;assembly=Eto.Wpf" 
                    xmlns:rwr="clr-namespace:RhinoWindows.Runtime">

    <SolidColorBrush x:Key="Button.Static.Background" Color="{DynamicResource RhinoTheme.Button.Enabled.Background}"/>
    <SolidColorBrush x:Key="Button.Static.Border" Color="{DynamicResource RhinoTheme.Button.Enabled.Border}"/>
    <SolidColorBrush x:Key="Button.Foreground" Color="{DynamicResource RhinoTheme.Button.Enabled.Text}"/>
    <SolidColorBrush x:Key="Button.MouseOver.Background" Color="{DynamicResource RhinoTheme.Button.EnabledHover.Background}"/>
    <SolidColorBrush x:Key="Button.MouseOver.Border" Color="{DynamicResource RhinoTheme.Button.EnabledHover.Border}"/>
    <SolidColorBrush x:Key="Button.MouseOver.Foreground" Color="{DynamicResource RhinoTheme.Button.EnabledHover.Text}"/>
    <SolidColorBrush x:Key="Button.Pressed.Background" Color="{DynamicResource RhinoTheme.Button.EnabledPressed.Background}"/>
    <SolidColorBrush x:Key="Button.Pressed.Border" Color="{DynamicResource RhinoTheme.Button.EnabledPressed.Border}"/>
    <SolidColorBrush x:Key="Button.Disabled.Background" Color="{DynamicResource RhinoTheme.Button.Disabled.Background}"/>
    <SolidColorBrush x:Key="Button.Disabled.Border" Color="{DynamicResource RhinoTheme.Button.Disabled.Border}"/>
    <SolidColorBrush x:Key="Button.Disabled.Foreground" Color="{DynamicResource RhinoTheme.Button.Disabled.Text}"/>

    <Style x:Key="ButtonStyle" TargetType="ButtonBase">
        <Setter Property="Background" Value="{DynamicResource Button.Static.Background}"/>
        <Setter Property="BorderBrush" Value="{DynamicResource Button.Static.Border}"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="HorizontalContentAlignment" Value="Center"/>
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="Padding" Value="1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ButtonBase">
                    <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="true">
                        <ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="rwr:EtoStyles.IsBorderless" Value="True">
                            <Setter Property="Background" TargetName="border" Value="Transparent"/>
                            <Setter Property="BorderThickness" TargetName="border" Value="0"/>
                            <Setter Property="BorderBrush" TargetName="border" Value="{x:Null}"/>
                        </Trigger>
                        <!-- since we have no border, set the image opacity when not enabled -->
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsEnabled" Value="False"></Condition>
                                <Condition Property="rwr:EtoStyles.IsBorderless" Value="True"></Condition>
                            </MultiTrigger.Conditions>
                            <Setter Property="Opacity" TargetName="border" Value="0.5"/>
                        </MultiTrigger>

                        <!-- Defaults -->
                        <Trigger Property="IsMouseOver" Value="true">
                            <Setter Property="Background" TargetName="border" Value="{DynamicResource Button.MouseOver.Background}"/>
                            <Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource Button.MouseOver.Border}"/>
                        </Trigger>
                        <Trigger Property="IsPressed" Value="true">
                            <Setter Property="Background" TargetName="border" Value="{DynamicResource Button.Pressed.Background}"/>
                            <Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource Button.Pressed.Border}"/>
                        </Trigger>
                        <Trigger Property="IsEnabled" Value="false">
                            <Setter Property="Background" TargetName="border" Value="{DynamicResource Button.Disabled.Background}"/>
                            <Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource Button.Disabled.Border}"/>
                            <Setter Property="TextElement.Foreground" TargetName="contentPresenter" Value="{DynamicResource Button.Disabled.Foreground}"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Resources>
            <Style TargetType="Label">
                <Setter Property="Foreground" Value="{DynamicResource Button.Foreground}"/>
                <Style.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Foreground" Value="{DynamicResource Button.MouseOver.Foreground}"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter Property="Foreground" Value="{DynamicResource Button.Disabled.Foreground}"/>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </Style.Resources>

    </Style>

    <Style TargetType="eto:EtoButton" BasedOn="{StaticResource ButtonStyle}">
        <Style.Triggers>
            <Trigger Property="IsDefaulted" Value="true">
                <Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
            </Trigger>
        </Style.Triggers>
    </Style>

    <Style TargetType="Button" BasedOn="{StaticResource ButtonStyle}">
        <Style.Triggers>
            <Trigger Property="IsDefaulted" Value="true">
                <Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
            </Trigger>
        </Style.Triggers>
    </Style>

    <Style x:Key="ButtonSpinnerRepeatButton" TargetType="{x:Type RepeatButton}" BasedOn="{StaticResource ButtonStyle}">
        <Setter Property="rwr:EtoStyles.IsBorderless" Value="true"/>
    </Style>


</ResourceDictionary>

Then apply it to your Eto window like so (requires RhinoWindows nuget reference):

var nativeWindow = myEtoWindow.ToNative();
nativeWindow.Resources.MergedDictionaries.Add(new ResourceDictionary { Source = new Uri("pack://application:,,,/MyFunAssembly;component/path/to/MyStyles.xaml", UriKind.RelativeOrAbsolute) });

Hope this helps!

2 Likes

Good job!

Hey @mrhe, this is some absolutely gorgeous UI/UX work.
I have so many questions, so I’ll try to break them down as much as possible.

#1 - The UI on right, is that entirely done in Eto.Forms.Drawable? Does this mean it’s all entirely custom components?
#1.1 - Could these Custom components be released as a nuget package? I’d love to collaborate on this kind of project
#1.2 - Is any of this Web stuff, html/css etc? Or all desktop
#1.3 - Is any of this using xeto?
#2 - Is this all cross platform?
#3 - When will you release this amazing plugin? Is it free or paid?
#4 - Is everything we see in the Rhino View Display Pipeline code, or have you messed with Open-GL?
#4.1 - How do you draw such lovely gradients?
#5 - I assume you’re using some nice MVVM, any resources on that?
#6 - Where did you learn all of this? Did you figure it all out, or are there some resources you’d recommend we also check out?
#7 - Is there any chance you could release any of the source code of the UI etc? I would LOVE to read through and see how all the magic is done :orange_heart:!

– cs

2 Likes

Thanks for the kind words!

To your questions:

  1. Yes, everything in the right panel are custom Eto.Drawables. Sliders, buttons, expanders etc. The Outliner is in part based on TreeGridView as well.

1.1 Maybe. A lot of work went into creating this UI and I’m not sure what to do with it. Keep it proprietary or release to the community. I’m open to suggestions on how to best approach it.

1.2 No html/css in this one. I experimented with WebView before but ended up deciding to go with purely desktop code.

1.3 No xeto either. Used it at the beginning but got discouraged by its limitations when it comes to rendering previews.

  1. It most probably is but I don’t have access to a Mac and haven’t tested it yet. This will be part of the alpha testing stage.

  2. We are aiming for a Q4 beta release. It will be free of charge followed by a Q1 2024 release of the 1.0 version which would be monetized. The exact pricing model still needs to be decided upon.

  3. The HUD controls are done via the DisplayPipeline. Mesh rendering including coloring is done in OpenGL.

4.1 This is OpenGL but you could easily get the same visual quality with VertexColors in the DisplayPipeline. We use OpenGL to keep things snappy while editing.

  1. Kind of. The UI framework has grown a lot and @Wiley is in the process of refactoring it to be decoupled from our app code. We’re getting there but it still requires some work.

  2. A lot of grind, really. This forum and Eto docs are your best bet. StackOverflow for more general questions. And reverse engineering how other frameworks handle things. As an example - I was recording Blender’s UI animations in 60 FPS and then analyzing frame by frame how elements are rearranged and in which order.

  3. See my answer to 1.1 above. I’m happy to share snippets and answer questions but am not quite ready to open-source all of the code. But I’m open to suggestions on how to move forward.

3 Likes

Thanks so much for such a full response @mrhe,

Okay awesome, so I’ll need to get comfy with Eto.Drawables! But anything is possible in Eto, how exciting :blush::blush::blush:.

I don’t think releasing the whole UI would necessarily be the best solution or even the most effective one, as much as it would be very helpful, it’d be A LOT of code to sift though, which certainly won’t help beginners, or collaboration.

What would be amazing honestly is generic samples. Small projects/snippets/UI parts that show how a concept/part works. How its created so that others can try creating what they want to make as well.
e.g;

  1. How is that Gradient slider made? :heart_eyes::heart_eyes::heart_eyes:
  2. How is the the tree view implemented?
  3. How do you re-arrange with the grabbers?
  4. How do the nicely coloured sliders work?
  5. How do you maintain a consistent and effective colour scheme?
  6. What’s an effective MVVM template/layout for this?
  7. How do you nest everything?
    … I could honestly list things I want to know for ever, but this is a good start on my list :blush:

I would really love to be able to write UIs at the level you have and make it easier to do so for others, so I’m hoping we can find a good way to explore all of this :yum:

– cs

1 Like

@csykes

Thanks for the questions, and I really love the enthusiasm!

  1. How is that Gradient slider made
  2. How is the the tree view implemented?
  3. How do you re-arrange with the grabbers?
  4. How do the nicely coloured sliders work?
  5. How do you nest everything?

All the controls you see are made and inherited from a few key Eto classes:

  • Drawable for all the controls you see.
  • DynamicLayout for arranging controls together and establishing hierarchy.
  • Panel for caching the control variables so the UI runs smoothly and fast.

As @mrhe has said, there has been a lot of effort put in for us to get to this point. The tree view (we call it the DraggableLayout) alone consists of about 700 lines of code. I cannot explain how it’s implemented in a few sentences, but here are a few pointers to help you know where to look:

  1. For the GradientControl: You can create a gradient brush with the LinearGradientBrush class, and then in the OnDraw() method, you call e.Graphics.FillPath(gradient, path);
  1. For SliderStepper and the DraggableLayout: We subscribe to the MouseEvent and watch for callbacks like MouseDown, MouseMove, etc. The value or the position of the control would then follow the mouse position.
  1. For the colors: As designers, we already developed some intuitions about what colors would work together. The key is to start from one accent color, and then vary the saturation and lightness of the accent color to establish hierarchy and importance.

This is how we get a pastel color tone for different modifiers:

        public static Eto.Drawing.Color GetRandomColor(int uniqueIndex)
        {
            Random random = new Random(uniqueIndex + 3);
            var r = random.Next(150, 240);
            var g = random.Next(140, 240);
            var b = random.Next(120, 220);
            return Eto.Drawing.Color.FromArgb(r, g, b);
        }
3 Likes