Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FR - Dynamic layout #410

Open
arjunmenon opened this issue Apr 24, 2018 · 28 comments
Open

FR - Dynamic layout #410

arjunmenon opened this issue Apr 24, 2018 · 28 comments
Assignees
Milestone

Comments

@arjunmenon
Copy link

Hey
As a nice way to create layouts I find the technique from TabrisJS, interesting.
As of now, Shoes needs to know the left, top, width, height to create a container. Many times creating seemingly simple 3-4 column layout with header or footers or attempting something adventurous, there is a lot of trial and error. In fact, we can't specify right on anything.

A nice way would be to tell shoes simply what the left,right,top,bottom margins are for a stack, and it reserves a container in the window. For other boxes we can simply ask Shoes to refer the previous stack and start allocating spaces, something like

stack(left:0, top:0, right: bottom:75%) {}
stack(left:0, top:0, right: bottom:50%) {}
stack(left:0, top:0, right: bottom:25%) {}
stack(left:0, top:0, right: bottom:0) {}

This will essentially create 4 vertical boxes (width and height auto calculated by shoes), based on the current height of the window., that is, a more responsive, easier to manage layout. No need to explicitly tell the width and height, when unsure. (Though it is still available as a property, if desired.)

Or if I want to create a centered 'card' with a footer, i can simply do

stack(left:10, right:10, bottom:10, top:10) {} # a stack 'card' with 10 point margin around it and centered
stack(left:0, right:0, bottom: 0, top: [prev(), 10]){} # this auto calculates the height of footer, and makes sure it is 10 margins below the card

So essentially, Shoes gets the ability to auto calculate width and height based off margins. This makes it easier to create layouts better than what you find if working solely with GTK.

An easier layout system makes it easy to create GUIs.

@ccoupe
Copy link

ccoupe commented Apr 24, 2018

Correct. Shoes does not have a tabular layout or a report layout. It's one of things that make Shoes easy. Adding new layouts is really, really, really hard. Anyone who has looked at that code has said "I'm not going in that hole!" No volunteers - no code. It's been and issue since Shoes was first written because Shoes was not written to do those sorts of things.

@arjunmenon
Copy link
Author

Can right property be added to elements? If I want to align an image on the right, I generally need to calculate the window width minus the image width plus margin, everytime. Bit annoying.

And it may be possible to do some dynamic calculation, without width and height, so if the margin is given, Shoes can estimate based off window dimensions where to place a container. Kind of like reverse estimation. It may be a simple subtraction in the Ruby world. Maybe bit more line in the C kingdom.

This is helpful and would be very important to layout positioning. Because to add footer, there is no easy way to estimate height of a footer slot, if the rest of the upper slot dimensions are not defined, like in a paragraph or API response.

@ccoupe
Copy link

ccoupe commented Apr 28, 2018

right is already a defined sytle for slots and elements, according to the manual.

@arjunmenon
Copy link
Author

Yeah but it doesn't work properly as I had raised an issue earlier. #402

@ccoupe
Copy link

ccoupe commented May 13, 2018

Things you can do with a user defined widget. Line formatter.

@ccoupe
Copy link

ccoupe commented Jun 12, 2018

New wiki article on a future layout (proposal)

@arjunmenon
Copy link
Author

In my humble opinion, integrating all of Gtk's layout techniques is too cumbersome.
While using Grids, it became very difficult to actually align elements to proper shape and positions.
You have also mentioned this in the wiki. Moreover, layouts are not responsive to windows sizes.

Using GtkBox is much better suited, but again there is a lot of config to do regarding H and V align.

The best way to actually work with Gtk is using Glade. That takes away massive amounts of child configuration, literally minimizing hours of customizing if using hard coded Gtk layout methods.

The problem - Glade for shoes, XML.
One solution - a shoes/slots or HTML type layout compiler to Glade XML.

We write HTML-like structure to define layouts in <div> elements, which also takes care of hierarchy. GTK3 also uses CSS3, so styling is possible. GtkBuilder is easy to work with. Reduces code complexity.

Advantage - most Ruby devs are web developers. Becomes easy for them to start working in a similar environment.

Disadvantages - none.

Ok, not entirely but not a disadvantage, but lot of work and potential rewrite. Do recognize writing a compiler is not a joke. But this is not impossible.
This can literally change how GUIs are developed.

@IanTrudel
Copy link
Collaborator

The best way to actually work with Gtk is using Glade.

I evaluated the possibility to use Glade but there are several challenges including the fact that it's not running on Windows, or at least, not the current version.

Also evaluated the possibility to have our own GUI builder. The main hurdle was to be able to catch events before they reach GTK widgets or other elements. @ccoupe provided a solution for that, see global events.

@arjunmenon
Copy link
Author

I came across this - https://github.com/libyui/libyui/
It is widget abstraction lib for Gtk, Qt and ncurses
There idea is to write the layout, that can be used in any Gui frontend. Issue is it is built for Opensuse, though installing for other systems(including windows) is supported but not at all documented.

Gtk should have a QML type markup language to make it easy to define the layout.

I tried something like this - https://gist.github.com/arjunmenon/e0a65e21b6789b03eea704660001a98f

This is a DSL to write XML.
At present, it just shows us a simple window. You write XML markup similar to what Glade will generate.

To make it absolutely durable, I need to abstract away the Glade structure and present a simple shoes-like style.

This involves lots of studying internal GTK and glade layers. Too advanced. Though I will surely give this a try and provide an update.

I did came across similar projects as well, but they expect you to write GTK classes and know all of them and are nowhere like Shoes

@IanTrudel
Copy link
Collaborator

You really have good ideas that would worth exploring but we are such a small team. Usually @ccoupe is the one to remind me that! Perhaps you could write a proof-of-concept and we can see whether or not we should invest time in this.

I tried something like this - https://gist.github.com/arjunmenon/e0a65e21b6789b03eea704660001a98f

Any way to test this in the context of Shoes?

https://github.com/karottenreibe/ruby-gtkbuilder (gtk dsl)

Last commit in 2012. No can do.

https://github.com/glurp/dsl-gtk (gtk dsl)

Worth looking at but the list of dependencies is rather long.

https://github.com/skyfex/greatk (same)

Last commit in 2015. No can do.

@ccoupe
Copy link

ccoupe commented Jun 12, 2018

If someone needs Glade/Gtkbuilder and all it's options and possibilities then they should probably use Ruby/Gtk3 bindings instead of Shoes and they already know all about gtk. Cocoa programmers would expect to use InterfaceBuilder files from xCode.

The grid layout example is not pretty (imo) but it's Shoes's like.

Any new layout method will have to be written from scratch in C using the existing x,y placement - all layouts devolve to using x,y placement. The question is what to do when a widget changes size or the container/layout changes size

It might be useful to think about a more extensible mechanism in shoes so that layout can be written in Ruby. I'm just thinking out loud. Maybe a class you subclass and you get a callback for adding a widget, and one to reflow/layout based on new size. the subclass has to manage an array of widget x,y,w,h and Shoes just draws them.

@IanTrudel
Copy link
Collaborator

IanTrudel commented Jun 12, 2018

If someone needs Glade/Gtkbuilder and all it's options and possibilities then they should probably use Ruby/Gtk3 bindings instead of Shoes and they already know all about gtk. Cocoa programmers would expect to use InterfaceBuilder files from xCode.

It's one of the reasons I started exploring the possibility to have a GUI builder written in Shoes. Shoes wouldn't be able to fully render everything produced by Glade. As you mentioned, Shoes and GTK have both their own ways to place things, which would enter in conflicts within Shoes.

@arjunmenon
Copy link
Author

Cocoa programmers would expect to use InterfaceBuilder files from xCode.

@ccoupe I understand. I dont have a Mac or windows machine, so as someone looking towards you, my focus naturally would extend to the system I am working now. Its completely 180 for you, which at this moment I lack due to obvious restrictions.
I am simply attempting to bounce some thoughts, hoping it might steer some conversation to a solution.

The question is what to do when a widget changes size or the container/layout changes size

We can emulate what web browsers do.
Consider two images side-by-side. If one of them becomes large, we can allot image 1 the extra space it wants and center align it and push image 2 just below it but centered.
Now web browsers majorly depend on media-queries to resolve these conflicts. So, this might be emulated in Shoes as well. Ask users to define what happens if window size changes and define media-queries for their respective widgets. That way, Shoes can look up what to do with the widgets or fall back to its defaults.

Shoes and GTK have both their own ways to place things, which would enter in conflicts within Shoes.

@backorder Hey what if Qt is given a chance. Its more welcoming to all platforms and bindings for it up-to date.

My only grudge with shoes is that some basic widgets are not available, like menubar, toolbar, tab panes, access to system tray, etc.
My big issues with Gtk is that layout is an absolute pain in the butt. There is not one class that holds all methods to properly configure just one widget. its always back-and-forth with parent and child classes, and its never obvious.

Have you had a look at libyui, that looks promising? That might ease-up a lot of issues.

@ccoupe
Copy link

ccoupe commented Jun 13, 2018

We can emulate what web browsers do.

That is the model for what flow and stacks does now.

Hey what if Qt is given a chance. Its more welcoming to all platforms and bindings for it up-to date.

Qt has/had an issue using cairo which Shoes3 depends on. Shoes4 folks say they just need someone to write the Qt backend which wouldn't be that difficult (and also removes the need for Java for them).

My only grudge with shoes is that some basic widgets are not available, like menubar, toolbar, tab panes, access to system tray, etc.

We need to get you to use Shoes 3.3 - the 3.3.7 beta has menus. systray appeared in 3.3.5, toolbar you can do yourself and you can probably do panes now. Shoes is not a simplified gtk or cocoa - don't think of it that way. Approach the design the Shoes way and things become easier.

@IanTrudel
Copy link
Collaborator

Hey what if Qt is given a chance. Its more welcoming to all platforms and bindings for it up-to date.

Qt has/had an issue using cairo which Shoes3 depends on. Shoes4 folks say they just need someone to write the Qt backend which wouldn't be that difficult (and also removes the need for Java for them).

Qt is a difficult choice because it may compromise any current or future commercial usage of Shoes. Qt requires commercial users to buy a license.

RE: cairo

I suggested in #389 (performance maintenance release) to evaluate Skia as a replacement for Cairo.

My only grudge with shoes is that some basic widgets are not available, like menubar, toolbar, tab panes, access to system tray, etc.

We have been slowly integrating such features. Some were by @ccoupe and some were by me. Help would be most welcomed.

@ccoupe ccoupe self-assigned this Jul 19, 2018
@ccoupe ccoupe added this to the 3.3.8 milestone Jul 19, 2018
@ccoupe
Copy link

ccoupe commented Jul 19, 2018

I've been thinking about a more general way of doing layouts in Shoes That would allow anyone who cares enough to write one in Ruby and if it turns out to be useful it could be converted to C if needed. On a personal note, I wouldn't be blamed for the choice of names ;-)

@ccoupe
Copy link

ccoupe commented Oct 7, 2018

Here's a bit of Shoes code.

class MyLayout 
  attr_accessor :pos_x, :pos_y
  
  def initialize()
    @pos_x = 25
    @pos_y = 25
  end
  
  def add_widget(widget)
    widget.move @pos_x, @pos_y
    @pos_x += 25
    @pos_y += 25
  end
end

Shoes.app width: 350, height: 400, resizeable: true do
  stack do
    para "Before layout"
    @ml = layout manager: MyLayout.new, width: 300, height: 300 do
      background yellow
      p1 = para "First Para"
      a = button "one"
      b = button "two"
      p2 = para "I am #{self}"
    end
  end
  para "After layout"
  para "@ml is #{@ml.inspect}"
end

It produces
userlayout

Of course there is a lot more to do and think about but sometimes a working example gets the thinking started.

ccoupe pushed a commit that referenced this issue Oct 7, 2018
* basically a flow that calls a delegate when adding an element.
* much more to do.
ccoupe pushed a commit that referenced this issue Oct 15, 2018
* beqin conversion of cassowary solver from python to ruby
@ccoupe
Copy link

ccoupe commented Oct 15, 2018

Is anyone looking for something to do? I want to convert the cassowary solver from python to ruby and I can use some help. Branch 'direwolf` has a copy of the python, ruby, and tests. About half the code is converted to Ruby, nonet of it has tests converted or run and 100% needs double checking.

@arjunmenon
Copy link
Author

arjunmenon commented Oct 15, 2018

Hey @ccoupe
I havent heard of cassowary solver before. I can study it, if I understand it properly I can try porting it. Let me give myself a week on this.

EDIT -
I found some Ruby projects -

  1. https://github.com/sfeu/cassowary
  2. https://github.com/timfel/cassowary-ruby

Are these what you are looking for?

@ccoupe
Copy link

ccoupe commented Oct 15, 2018

Whoa ! I did a search for Ruby versions before I tried to convert and I found nothing. Thank You! Yes, one of those will work well enough (I think) for now. It's not a simple layout language so it's worth you study in case it does end up as the 'advanced option' for Shoes. There is a VFL language from Apple and I've got a C version of a parser for that.

@ccoupe
Copy link

ccoupe commented Oct 16, 2018

Even better yet, there are two gems, cassowary-ruby which a pure ruby (this smalltalk conversion mentioned as #2 above. The second gem appears to be a wrapper of the C++ library. That doesn't build on OSX (yet) but that's just a small matter of figuring out where things go. (and cross compiling for windows - not a show stopper). The pure ruby is sufficient for my test purpose. The internal algorithm is beyond my knowledge level to tinker with. Thanks @arjunmenon

@ccoupe
Copy link

ccoupe commented Oct 17, 2018

Fun. I've wired the cassowary-ruby algo to the user-layout mechanism. Before and after:
l3-before
l3-after
The code is pretty ugly but it's never going to be simple. It's not a simple algorithm.

Shoes.setup do
    gem 'cassowary-ruby'
end
require 'cassowary'
include Cassowary
class CassowaryLayout 
  attr_accessor :canvas, :widgets, :solver, :attrs, :left_limit,
	:right_limit, :top_limit, :height_limit
  
  def initialize()
    $stderr.puts "initialized"
    @widgets = {}
    @solver = SimplexSolver.new
	@attrs = {}
  end
  
  def setup(canvas, attr)
    @canvas = canvas
    wid = attr[:width]
    hgt = attr[:height]
    @left_limit = Variable.new(name: 'left', value: 0.0)
   @right_limit = Variable.new(name: 'width', value: wid)
   @top_limit = Variable.new(name: "top", value: 0.0)
   @height_limit = Variable.new(name: "height", value: hgt)
   @solver.add_stay(@left_limit)
   @solver.add_stay(@right_limit, Strength::WeakStrength)
    $stderr.puts "callback: setup #{wid} X #{hgt}"
  end
  
  def add(canvas, widget)
   # widget is not on-screen and allocated at this time. Pity
   $stderr.puts "callback add: #{widget.class} #{canvas.contents.size}"
  end
  
  def track(obj, str)
    @widgets[str] = obj
    vars = {}
    vars['left'] = Variable.new(name: 'left'+str, value: 0)
    vars['width'] = Variable.new(name: 'width'+str, value: 0)
    vars['top'] = Variable.new(name: 'top'+str, value: 0)
    vars['height'] = Variable.new(name: 'height'+str, value: 0)
    @attrs[str] = vars
  end
  
  # parse string like 'b1-width' and return Cassowary Variable
  def var(str)
     str[/(\w+)\-(\w+)/]
     @attrs[$1][$2]
  end
  
  def clear()
    $stderr.puts "callback: clear"
  end
  
 
  def finish()
    @widgets.each_pair do |k, widget|
	left = widget.left
	top = widget.top
	height = widget.height
	width = widget.width
	$stderr.puts "At #{left},#{top}"
	left = @attrs[k]['left'].value.to_i
	width = @attrs[k]['width'].value.to_i
	$stderr.puts "move to #{left}, #{top} for w:#{width}"
	widget.style width: width
	widget.move(left, top)
    end
  end
  
  # send other methods to @solver
  def method_missing(meth, *args, &block)
    if @solver.respond_to? meth
	@solver.send(meth, *args, &block)
    else
	super
    end
  end
  
end

Shoes.app width: 480, height: 280, resizeable: true do
  stack do
    @p = para "Before layout"
    @ml = CassowaryLayout.new
    @lay = layout manager: @ml, width: 450, height: 200  do
      background teal
      a = button "one"
      @ml.track(a, 'b1')
      b = button "two"
      @ml.track(b, 'b2')
    end
    @p.text = @lay.inspect    
    # The two buttons are the same width
    @ml.add_constraint(@ml.var('b1-width').cn_equal @ml.var('b2-width'))
	
    # Button1 starts 50 from the left margin.
    @ml.add_constraint(@ml.var('b1-left').cn_equal @ml.left_limit + 50)
	
    # Button2 ends 50 from the right margin 
    @ml.add_constraint((@ml.left_limit + @ml.right_limit).cn_equal @ml.var('b2-left') + @ml.var('b2-width') + 50)

    # Button2 starts at least 100 from the end of Button1. This is the
    # "elastic" constraint in the system that will absorb extra space
    # in the layout.
   @ml.add_constraint(@ml.var('b2-left').cn_equal @ml.var('b1-left') + @ml.var('b1-width') + 100)

    # Button1 has a minimum width of 87
    @ml.add_constraint(@ml.var('b1-width').cn_geq 87)
	
    # Button1's preferred width is 87
    @ml.add_constraint(@ml.var('b1-width').cn_equal 87, Strength::StrongStrength)
	
    # Button2's minimum width is 113
    @ml.add_constraint(@ml.var('b2-width').cn_geq 113)

    # Button2's preferred width is 113
    @ml.add_constraint(@ml.var('b2-width').cn_equal 113, Strength::StrongStrength)
  end
  button "refresh" do
    @lay.finish
  end
  button "shrink" do
    wid = @lay.width * 0.9
    @lay.width = wid
  end
  button "expand" do
  end
end

@arjunmenon
Copy link
Author

Hi
I am getting a no method error for layout

Error in <unknown> line 0 | 2018-10-17 13:22:11 +0530
undefined method `layout' for (Shoes::Types::App "Shoes")
cassowary.rb:86:in `method_missing'
cassowary.rb:86:in `block (2 levels) in <main>'
cassowary.rb:83:in `call'
cassowary.rb:83:in `stack'
cassowary.rb:83:in `block in <main>'
eval:1:in `instance_eval'
eval:1:in `block in <main>'
cassowary.rb:82:in `call'
cassowary.rb:82:in `app'
cassowary.rb:82:in `<main>'
/home/arjun/.shoes/federales/lib/shoes.rb:527:in `eval'
/home/arjun/.shoes/federales/lib/shoes.rb:527:in `visit'
/home/arjun/.shoes/federales/lib/shoes.rb:167:in `show_selector'
/home/arjun/.shoes/federales/lib/shoes.rb:211:in `block (4 levels) in splash'
-e:1:in `call'

Shoes Federales 3.2.25.r2170

@ccoupe
Copy link

ccoupe commented Oct 17, 2018

@arjunmenon , That's because the layout method is an experiment in Shoes 3.3.8. The only way to get it is to clone the git repo and build Shoes from source which requires some command line skills and some time to set it up. Non trivial on Windows unless you happen to be running MSYS2. Contact me ccoupe@cableone.net if you'd like some help with that.

As an experimental feature, it's subject to change. I've changed the api twice today and there are missing capabilities that would be a show stopper if they can't be implemented.

Shoes 3.2.25 is very old. That's 6 or 7 releases back. Shoes 3.3.6 is current and 3.3.7 beta is available.

@ccoupe
Copy link

ccoupe commented Oct 21, 2018

The api is stable enough for folks to attempt to use. There is some documentation and the git repo on branch 'direwolf' has Tests/layout/l3.rb - note the cassowary gem used is not that easy to work with, IMO. It's good enough to play with and study more. To that end, I've uploaded Shoes 3.3.8 to the beta site for those who don't want to build from source.

@backorder , and others, now would be a good time to criticize the api choice of names.

ccoupe pushed a commit that referenced this issue Oct 24, 2018
* independent of solver except for using standard terminalogy
  which Shoes doesn't provide. Like 'super' where we have 'canvas'
  Clever person could figure out how to get vfl constraints mapped
  to cassowary-ruby gem.
ccoupe pushed a commit that referenced this issue Oct 30, 2018
* compiles and links linux, osx, mxe (not xwin7 - needs newer glib)
* doesn't run
ccoupe pushed a commit that referenced this issue Nov 7, 2018
* It compiles and links and runs but doesn't do much visually.
ccoupe pushed a commit that referenced this issue Nov 7, 2018
* for this commit, just enough to get the Gtk out.
@ccoupe
Copy link

ccoupe commented Nov 13, 2018

I took a break from getting C cassowary working and wrote a quick grid layout manager in Ruby.
grid
There's some 'issues' but it was quick:

# Tests/layout/l4.rb - grid layout

class GridLayout

  attr_accessor :ncol, :nrow, :colsz, :rowsz, :widgets, :hpad, :vpad

  def initialize(attr = {})
    @ncol = 0
    @nrow = 0
    @colsz = 0
    @rowsz = 0
    @widgets = []
    @hpad = 2
    @vpad = 2
  end
  
  def setup(canvas, attr)
  end

  def add(canvas, ele, attr)
    col = attr[:col] ? attr[:col]-1 : 0
    row = attr[:row] ? attr[:row]-1 : 0
    rspan = attr[:rspan] ? attr[:rspan] : 1
    cspan = attr[:cspan] ? attr[:cspan] : 1
    native = attr[:native] ? attr[:native] : false
    @ncol = [@ncol, col + cspan].max
    @nrow = [@nrow, row + rspan].max
    widgets << {ele: ele, col: col, row: row, cspan: cspan, rspan: rspan,
      native: native}
   end
  
  def remove
  end
  
  def clear
  end
    
  def size (canvas, pass)
    return if pass == 0
    @rowsz = (canvas.height / @nrow).to_i
    @colsz = (canvas.width / @ncol).to_i
    @widgets.each do |entry| 
      x = entry[:col] * @colsz + @hpad
      y = entry[:row] * @rowsz + @vpad
      w = entry[:cspan] * @colsz - @hpad
      h = entry[:rspan] * @rowsz - @vpad
      widget = entry[:ele]
      widget.move(x, y)
      return if entry[:native]
      if entry[:cspan]
        if widget.width != w 
          widget.style width: w
        end
      end
      if entry[:rspan]
        if widget.height != h
          widget.style height: h
        end
      end
    end
  end
  
  def finish
  end
      
end  

Shoes.app width: 400, height: 400 do 
  stack do
    para "Before Grid"
    grid = GridLayout.new hpad: 2, vpad: 2
    @lay = layout use: grid, width: 300, height: 305 do
      background aliceblue
      button "one", col: 1, row: 1, cspan: 2, rspan: 1 
      button "two", col: 3, row: 1, cspan: 2, rspan: 2
      para "Long String of characters", col: 2, row: 5, cspan: 2
      button "three", col: 2, row: 3, native: true
    end
    para "After Grid"
  end
end

@ccoupe
Copy link

ccoupe commented Nov 14, 2018

Updated the grid example and code. Documented somewhat

ccoupe pushed a commit that referenced this issue Nov 24, 2018
* cassowary-ruby gem may be working correctly - it's hard to KNOW
  can use vfl_parser
* native cassowary (emeus) is not working - but now I have something
  to compare with.
@ccoupe
Copy link

ccoupe commented Nov 24, 2018

Some success with the cassowary-gem and user written layouts.

require 'layout/cassowary'

Shoes.app width: 500, height: 400, resizeable: true do
  stack do
    para "Test vfl parser"
    @cls = CassowaryLayout.new()
    @lay = layout use: @cls, width: 450, height: 300 do
      background cornsilk
      para "OverConstrained", name: 'para1'
      edit_line "one", name: 'el1'
      button "two", name: 'but1'
      button "three", name: "but2"
      button "four", name: "but3"
    end
    @lay.start {
      metrics = {
        padding: 80.7
      }
      lines = [
        "H:|-[para1(but1)]-[but1]-|",
        "H:|-[el1(but2)]-[but2]-|",
        "H:[but3(but2)]-|",
        "V:|-[para1(el1)]-[el1]-|",
        "V:|-[but1(but2,but3)]-[but2]-[but3]-|"
      ]
      if @lay.vfl_parse lines: lines, views: @cls.contents, metrics: metrics
        constraints = @lay.vfl_constraints
        @lay.finish constraints 
     end
    }
 end
  para "After layout"
end

cas2

One can argue that it isn't correct for the given VFL given but I suspect it is. So we have two workable user written layouts, grid and cassowary.

ccoupe pushed a commit that referenced this issue Dec 8, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants