Terminal.Gui icon indicating copy to clipboard operation
Terminal.Gui copied to clipboard

Revamp built-in Scheme's (e.g. what *is* `Toplevel` anyway?).

Open tig opened this issue 7 months ago • 8 comments

I am not going to tacklet this as part of

  • #4062

Instead, we'll do it as a follow-on PR.

Recall this conversation: https://github.com/gui-cs/Terminal.Gui/issues/457#issuecomment-2832945208

Here's the latest Scheme Deep Dive that matches the latest impl in

  • #4062

Scheme Deep Dive

See Drawing for an overview of the drawing system and Configuration for an overview of the configuration system.

Scheme Overview

A Scheme is named a mapping from VisualRoles (e.g. VisualRole.Focus) to Attributes, defining how a View should look based on its purpose (e.g. Menu or Dialog). @Terminal.Gui.SchemeManager.Schemes is a dictionary of Schemes, indexed by name.

A Scheme defines how Views look based on their semantic purpose. The following schemes are supported:

Scheme Name Description
Base The base scheme used for most Views.
TopLevel The application Toplevel scheme; used for the Toplevel View.
Dialog The dialog scheme; used for Dialog, MessageBox, and other views dialog-like views.
Menu The menu scheme; used for Terminal.Gui.Menu, MenuBar, and StatusBar.
Error The scheme for showing errors, such as in ErrorQuery.

@Terminal.Gui.SchemeManager manages the set of available schemes and provides a set of convenience methods for getting the current scheme and for overriding the default values for these schemes.

var scheme = SchemeManager.GetCurrentSchemes () ["TopLevel"];

ConfigurationManager can be used to override the default values for these schemes and add additional schemes.

Flexible Scheme Management in Terminal.Gui.View

A View's appearance is primarily determined by its Scheme, which maps semantic VisualRoles (like Normal, Focus, Disabled) to specific Attributes (foreground color, background color, and text style). Terminal.Gui provides a flexible system for managing these schemes:

  1. Scheme Inheritance (Default Behavior):

    • By default, if a View does not have a Scheme explicitly set, it inherits the Scheme from its SuperView (its parent in the view hierarchy).
    • This cascading behavior allows for consistent styling across related views. If no SuperView has a scheme, (e.g., if the view is a top-level view), it ultimately falls back to the "Base" scheme defined in SchemeManager.GetCurrentSchemes().
    • The GetScheme() method implements this logic:
      • It first checks if a scheme has been explicitly set via the _scheme field (see point 2).
      • If not, and if SchemeName is set, it tries to resolve the scheme by name from SchemeManager.
      • If still no scheme, it recursively calls SuperView.GetScheme().
      • As a final fallback, it uses SchemeManager.GetCurrentSchemes()["Base"]!.
  2. Explicit Scheme Assignment:

    • You can directly assign a Scheme object to a View using the View.Scheme property (which calls SetScheme(value)). This overrides any inherited scheme. The HasScheme property will then return true.
    • Alternatively, you can set the View.SchemeName property to the name of a scheme registered in SchemeManager. If Scheme itself hasn't been directly set, GetScheme() will use SchemeName to look up the scheme. This is useful for declarative configurations (e.g., from a JSON file).
    • The SetScheme(Scheme? scheme) method updates the internal _scheme field. If the new scheme is different from the current one, it marks the view for redraw (SetNeedsDraw()) to reflect the visual change. It also handles a special case for Border to ensure its scheme is updated if it HasScheme.
  3. Event-Driven Customization: The scheme resolution and application process includes events that allow for fine-grained control and customization:

    • GettingScheme Event (View.Scheme.cs):

      • This event is raised within GetScheme() before the default logic (inheritance, SchemeName lookup, or explicit _scheme usage) fully determines the scheme.
      • Subscribers (which could be the SuperView, a SubView, or any other interested component) can handle this event.
      • In the event handler, you can:
        • Modify the scheme: Set args.NewScheme to a different Scheme object.
        • Cancel default resolution: Set args.Cancel = true. If canceled, the Scheme provided in args.NewScheme (which might have been modified by the handler) is returned directly by GetScheme().
      • The OnGettingScheme(out Scheme? scheme) virtual method is called first, allowing derived classes to provide a scheme directly.
    • SettingScheme Event (View.Scheme.cs):

      • This event is raised within SetScheme(Scheme? scheme) before the _scheme field is actually updated.
      • Subscribers can cancel the scheme change by setting args.Cancel = true in the event handler.
      • The OnSettingScheme(in Scheme? scheme) virtual method is called first, allowing derived classes to prevent the scheme from being set.
  4. Retrieving and Applying Attributes for Visual Roles (View.Attribute.cs): Once a View has determined its active Scheme (via GetScheme()), it uses this scheme to get specific Attributes for rendering different parts of itself based on their VisualRole.

    • GetAttributeForRole(VisualRole role):

      • This method first retrieves the base Attribute for the given role from the View's current Scheme (GetScheme()!.GetAttributeForRole(role)).
      • It then raises the GettingAttributeForRole event (and calls the OnGettingAttributeForRole virtual method).
      • Subscribers to GettingAttributeForRole can:
        • Modify the attribute: Change the args.NewValue (which is passed by ref as schemeAttribute to the event).
        • Cancel default behavior: Set args.Cancel = true. The (potentially modified) args.NewValue is then returned.
      • Crucially, if the View is Enabled == false and the requested role is not VisualRole.Disabled, this method will recursively call itself to get the Attribute for VisualRole.Disabled. This ensures disabled views use their designated disabled appearance.
    • SetAttributeForRole(VisualRole role):

      • This method is used to tell the ConsoleDriver which Attribute to use for subsequent drawing operations (like AddRune or AddStr).
      • It first determines the appropriate Attribute for the role from the current Scheme (similar to GetAttributeForRole).
      • It then raises the SettingAttributeForRole event (and calls OnSettingAttributeForRole).
      • Subscribers can modify the schemeAttribute (via args.NewValue) or cancel the operation (args.Cancel = true).
      • If not canceled, it calls Driver.SetAttribute(schemeAttribute).
    • SetAttribute(Attribute attribute):

      • This is a more direct way to set the driver's current attribute, bypassing the scheme and role system. It's generally preferred to use SetAttributeForRole to maintain consistency with the Scheme.

Impact of SuperViews and SubViews via Events

  • SuperView Influence: A SuperView can subscribe to its SubView's GettingScheme or GettingAttributeForRole events. This would allow a SuperView to dynamically alter how its children determine their schemes or specific attributes, perhaps based on the SuperView's state or other application logic. For example, a container view might want all its children to adopt a slightly modified version of its own scheme under certain conditions.

  • SubView Influence (Less Common for Scheme of Parent): While a SubView could subscribe to its SuperView's scheme events, this is less typical for influencing the SuperView's own scheme. It's more common for a SubView to react to changes in its SuperView's scheme if needed, or to manage its own scheme independently.

  • General Event Usage: These events are powerful for scenarios where:

    • A specific View instance needs a unique, dynamically calculated appearance that isn't easily captured by a static Scheme object.
    • External logic needs to intercept and modify appearance decisions.
    • Derived View classes want to implement custom scheme or attribute resolution logic by overriding the On... methods.

In summary, Terminal.Gui offers a layered approach to scheme management: straightforward inheritance and explicit setting for common cases, and a robust event system for advanced customization and dynamic control over how views derive and apply their visual attributes. This allows developers to achieve a wide range of visual styles and behaviors.

Proposal

Change the schemes to these:

Scheme Name Description
Base The foundational scheme used for most View instances. All other schemes inherit from this.
Form Used for form-like Views such as Dialog, MessageBox, and Wizard.
Command Used by Menu, MenuBar, and StatusBar to style command surfaces.
Error Used to indicate error states, such as ErrorQuery or failed validations.
Popover Used for lightweight, transient UI like hover text, help popups, or dropdowns.

tig avatar May 15 '25 17:05 tig

@tznind and @BDisp - Assuming you've read the above and understand the new View.Scheme API thats in

  • #4062

I have a question:

Right now View.Scheme (in #4062) looks like this:

   /// <summary>
   ///     Gets or sets the name of the Scheme to use for this View. If set, it will override the scheme inherited from the
   ///     SuperView. If <see cref="Scheme"/> was explicitly set (<see cref="HasScheme"/> is <see langword="true"/>),
   ///     this property will be ignored.
   /// </summary>
   public string? SchemeName { get; set; }

   // Both holds the set Scheme and is used to determine if a Scheme has been set or not
   private Scheme? _scheme;

   /// <summary>
   ///     Gets whether <see cref="Scheme"/> has been explicitly set for this View.
   /// </summary>
   public bool HasScheme => _scheme is { };

   /// <summary>
   ///     Gets or sets the Scheme for this view.
   ///     <para>
   ///         If the Scheme has not been explicitly set (<see cref="HasScheme"/> is <see langword="false"/>), this property
   ///         gets
   ///         <see cref="SuperView"/>'s Scheme.
   ///     </para>
   /// </summary>
   public Scheme Scheme
   {
       get => GetScheme ();
       set => SetScheme (value);
   }

   /// <summary>
   ///     Gets the Scheme for the View. If the Scheme has not been explicitly set (see <see cref="HasScheme"/>), gets
   ///     <see cref="SuperView"/>'s Scheme.
   /// </summary>
   /// <returns></returns>
   public Scheme GetScheme ()
   {
...
   }

   /// <summary>
   ///     Sets the Scheme for the View. Raises <see cref="SettingScheme"/> event before setting the scheme.
   /// </summary>
   /// <param name="scheme">
   ///     The scheme to set. If <see langword="null"/> <see cref="HasScheme"/> will be
   ///     <see langword="false"/>.
   /// </param>
   /// <returns><see langword="true"/> if the scheme was set.</returns>
   public bool SetScheme (Scheme? scheme)
   {
 ...
   }

Having Scheme be a property is troublesome. I'd like to remove it and have developers use GetScheme()/SetScheme()

There's already so much churn in #4062, i'm personally not too worried about how this impacts existing code. But I wanted to check with you before I make it so.

Thanks.

tig avatar May 15 '25 18:05 tig

If a property doesn't directly hold a value and need always to call methods to get and set a private field, I personally prefer to use public methods to get and set a private field and thus will force the user to catch a value from calling GetScheme/SetScheme methods and use it instead of constantly call the Scheme.get/set property that will always call the respective methods. Thus, the Scheme property can be removed.

BDisp avatar May 15 '25 18:05 BDisp

Seems fine to me, I've been trying to. Read the new docs starting with Configuration but there's a lot! From what I have read though it all makes sense and is good and understandable too

tznind avatar May 15 '25 19:05 tznind

I just made it private and wow, what a great way to find bugs where code is using Scheme incorrectly!

tig avatar May 15 '25 21:05 tig

I'm really excited about these changes after reading this deep dive! Very clear and easy to understand.

SuperView Influence: A SuperView can subscribe to its SubView's GettingScheme or GettingAttributeForRole events. This would allow a SuperView to dynamically alter how its children determine their schemes or specific attributes, perhaps based on the SuperView's state or other application logic. For example, a container view might want all its children to adopt a slightly modified version of its own scheme under certain conditions.

This will make the way I set SubView schemes so much cleaner! 😲

YourRobotOverlord avatar May 16 '25 02:05 YourRobotOverlord

Via @BDisp

Yeah. On this, I keep thinking "why do we have both Base and Toplevel schemes". In what world does anyone want both?

I'm only thinking of one possibility. Maybe I'm wrong but when you define a schema for Window and it's the Application.Top, all other Window sub-views will inherit the same schema, right? If affirmative a user may want a different schema for the Application.Top and other schema for the Window sub-views. In this case it's an advantage having the Toplevel schema. If it's possible to have different schemas for the same Window type then the Toplevel may be removed.

tig avatar May 28 '25 18:05 tig

Here's my latest proposal:

Scheme Name Description
Base The foundational scheme used for most View instances. All other schemes inherit from this.
Form Used for form-like Views such as Dialog, MessageBox, and Wizard.
Command Used by Menu, MenuBar, and StatusBar to style command surfaces.
Error Used to indicate error states, such as ErrorQuery or failed validations.
Popover Used for lightweight, transient UI like hover text, help popups, or dropdowns.

tig avatar May 28 '25 18:05 tig

I ran this experiment in Arrangement scenario:

        DatePicker datePicker = new ()
        {
            X = 30,
            Y = 17,
            Id = "datePicker",
            Title = "Not _Sizeable",
            ShadowStyle = ShadowStyle.Transparent,
            BorderStyle = LineStyle.Double,
            TabStop = TabBehavior.TabGroup,
            Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped
        };

        datePicker.SetScheme (new Scheme (
                                          new Attribute (
                                                         SchemeManager.GetScheme (Schemes.Toplevel).Normal.Foreground.GetHighlightColor (),
                                                         SchemeManager.GetScheme (Schemes.Toplevel).Normal.Background.GetHighlightColor (),
                                                         SchemeManager.GetScheme (Schemes.Toplevel).Normal.Style)));

Image

To me, this illustrates that it's super easy to adjust the scheme based on an existing scheme (in this case Toplevel). Thus we need LESS scheme's not more.

tig avatar May 28 '25 18:05 tig