Jump to content

Some Dynamic GUI Code


photo

Recommended Posts

I've been wanting a way to work with `UserInterface` without .ui files.  It looks like this is down in the pipeline, so I wrote a little code to help with this. Sharing it here.

Writing and reading to a cache directory is easy. But a lot of the value in UI is event binding. Turns out this is pretty simple too.
Binding is specified in the XML with a `callback` child element that specifies the event type using a `type` attribute. The handler is specified in the element content.
Handlers are only supported in script. The parser is forgiving though and ignores what it doesn't understand. We can add attributes we need and run binding (or other) code against the XML.
For example, an element to invoke a standard service provider (Provider, interface, Operation) can look like this:

<callback type="Clicked" service="Provider::Interface::Operation" />

The parser ignores the service attribute. We can put whatever we want on the elements. Like "action".

<button export="1"  name="MyButton">
  <text>Click Me</text>
  <callback type="Clicked" action="clickActionHandler" />
</button>

In our code we can use raw string literals to dynamically build a gui file.

interface vals {
  internal const String height = "60";
  internal const String width = "100";
  }
String xml => $$"""
<ui version="1.0">
  <window export="1" name="MainMenu" color="#808080" height="{vals.height}" width="{vals.width}" align="left" >
  <vbox align="expand" >
    <button export="1" name="ActionButton" align="expand">
      <text>Work</text>
      <callback type="Clicked" action="log" />
    </button>
  </vbox>
  </window>
</ui>
"""
;  

The xml string is used by code like this will save the file to disk and retrieve it. Retrieval is a two-step process. The UI constructor takes the path and builds the UI, then an XElement is created by a load of the same file. The XElement is passed to the binding code.

static class Basic {
  interface lex {
      internal const String action = "action";
      internal const String type = "type";
      internal const String name = "name";
  	  }
  
   interface Config {
      // Parameters
      internal const String GuiCache = "\\gui\\cache";
      internal const String GuiExt = ".gui.ui";

      // Computed
      //internal static String CachePath => $"{Unigine.Engine.DataPath}{GuiCache}";
      internal static String CachePath => $"{Environment.CurrentDirectory}{GuiCache}";
      }

   interface Helpers {
      static String GuiPath(String uiName) => $"{Config.CachePath}\\{uiName}{Config.GuiExt}";
      static Gui.CALLBACK_INDEX GetIndex(String name) => Enum.Parse<Gui.CALLBACK_INDEX>(name.ToUpper());
      }

   // Write to disk
   static void CacheUI(String uiName, String xml) {
      try { System.IO.File.WriteAllText(Helpers.GuiPath(uiName), xml); }
      catch (Exception e) { Debug.WriteLine(e.Message); }
      }

   // Read from disk
   static void FetchUI(out UserInterface ui, out XElement spec, String uiName, Gui gui) {
      ui = new(gui, Helpers.GuiPath(uiName));
      spec = XElement.Load($"{Config.CachePath}\\{uiName}{Config.GuiExt}");
      MapCallbacks(ui, spec);
      }

   // Common Handlers
   interface Receivers {
      static void CB1(Widget widget) {
         XElement elm = XElement.Parse(widget.Data);
         var alias = elm.Required(lex.action);
         EventMap[alias].Invoke(elm);
         }
      }

   // Event Handlers
   static void Log(XElement data) {
      Unigine.Console.WriteLine($"""
         Event {data.Required("type")}
         occured on {data.Required("name")}
         """
         );
      }

   // Event Mapper
   static Dictionary<String, Action<XElement>> EventMap = new() {
         { "log", Log }
      };

   // Callback Mapper
   static void MapCallbacks(UserInterface ui, XElement spec) {
      foreach (var callspec in spec.Descendants("callback")) {
         XElement parent = callspec.Parent ?? throw new();
         String name = parent.Required(lex.name);
         String callback = callspec.Required(lex.type);
         String action = callspec.Required(lex.action);
         Widget widget = ui.Widget(name);
         widget.Data = $"""
         <event-data
         name="{name}"
         type="{callback}"
         action="{action}" 
         />
         """.ToLine().Compress();

         _ = Helpers.GetIndex(callback) switch {
            Gui.CALLBACK_INDEX.CLICKED => widget.AddCallback(Gui.CALLBACK_INDEX.CLICKED, Receivers.CB1),
            _ => throw new NotImplementedException()
            };
         }
      }

  	// Extensions
    public static String Required(this XElement e, String name)
      => (string)(e.Attribute(name) ?? throw new());

    public static Unigine.Widget Widget(this UserInterface ui, String Name)
      => ui.GetWidget(ui.FindWidget(Name));

    public static String ToLine(this String str)
      => String.Join(" ", str.Split(new[] { '\r', '\n' }, RemoveEmptyEntries | TrimEntries));

    public static String Compress(this String input)
      => Regex.Replace(input, @"\s+", " ");
   }

 

Edited by Tessalator
added lex and fixed callback name
  • Like 2
Link to comment
×
×
  • Create New...