Adding UI elements in vanilla with auto positioning

Hello, as a huge fan of auto positioning in vanilla, I am trying to figure out how to add UI elements to an auto-layouted view. I just cannot get it to work. The problem seems to be that I cannot “update” the auto positioning rules, only add to them. Take this script for example:

from vanilla import *

class Demo:
	def __init__(self):
		self.w = FloatingWindow((1, 1), "Demo")
		
		self.w.button = Button("auto", "Button", callback=self.callback_function)
		
		rules = [
			"H:|-margin-[button]-margin-|",
			"V:|-margin-[button]-margin-|"
		]
		
		self.w.addAutoPosSizeRules(rules, {"margin": 10})
		self.w.open()
		self.w.makeKey()
		
	def callback_function(self, sender):
		self.w.button2 = Button("auto", "Button 2")
		rules = [
			"H:|-margin-[button2]-margin-|",
			"V:|-margin-[button]-margin-[button2]-margin-|",
		]
		
		self.w.addAutoPosSizeRules(rules, {"margin": 10})
		
Demo()

Clicking the first button adds a second button and attempts to update the auto positioning rules accordingly. This works for the horizontal rules, but not for the vertical ones – because the first rule is still applied, wherein the first button defines the vertical constraints of the window. The second button thus doesn’t fit.

Does anybody have any clue how to work with this? I don’t want to re-write everything with frame layout, that’s for amateurs :wink:

Any pointers as to where to find answers otherwise are also appreciated, I cannot find a resource apart from the vanilla docs, where this is not mentioned. Many thanks!

Consider using a VerticalStackView with just a single button and then adding the second button with appendView. I have not tried this, but it should still interoperate with any outside Auto Layout code.

1 Like

Thanks a million! That works great. Not quite what I was looking for, but I was simply looking for the wrong thing (coding it all myself), instead of finding a pre-made solution right out of the box.

It’s theoretically possible to do the same by manually removing/deactivating layout constraints and then adding/activating them again when needed, but that is cumbersome and error-prone. Stack views do the same thing – they also use Auto Layout under the hood – just nicely packaged so that you don’t need to do all the bookkeeping yourself.

Could you tell me where I can find some documentation on how to update layout constraints? I am so far only using vanilla’s addAutoPosSizeRules but have scoured various documentations for signs of other methods in vain.

As far as I know, there is no nice way to do it with Vanilla; you have to drop down to the AppKit API and use its Auto Layout methods directly. Generally, I would advice against doing that. Even Apple heavily promotes using stack and grid views over doing the same using manual Auto Layout constraints. But, if you must, have a look at the Auto Layout Guide by Apple:

I was just curious, in order to be able to do it myself. But I’m having great fun with VerticalStackView, it works great. Thanks so much for your help! :slightly_smiling_face:

Sorry to be following up on this again, but there seems to be a problem with callbacks – I added some buttons to my VerticalStackView, with a callback function defined. It doesn’t appear to be called anymore when clicking the buttons. In fact, nothing seems to be called anymore. Buttons, EditText, PopUpButtons, nothing works. Is there something that needs to be done especially?

Buttons outside of the stack view still work.

What’s more, all my PopUpButton lists within the stack view are greyed out.
image

Something seems to be fundamentally broken. If you nest the views in a loop, the callbacks stop working:

from vanilla import Button, VerticalStackView, FloatingWindow


class Demo:
	def __init__(self):
		self.w = FloatingWindow((1, 1))
		
		metrics = {
			"margin": 10
		}
				
		for number in ["one", "two"]:
			group = Group("auto")
			
			button1 = Button("auto", "Button", callback=self.button_callback)
		
			group.verticalStack = VerticalStackView("auto",
				views=[
					dict(view=button1),
				],
				spacing=10,
				edgeInsets=(10, 10, 10, 10),
			)
			
			group_rules = [
				"H:|-margin-[horizontalStack]-margin-|",
				"V:|-margin-[horizontalStack]-margin-|",
			]
			
			group.addAutoPosSizeRules(group_rules, metrics)
			
			setattr(self.w, number, group)
			
		rules = [
			"H:|[one]|",
			"H:|[two]|",
			"V:|[one][two]|",
		]
		
		self.w.addAutoPosSizeRules(rules, metrics)
		
		self.w.open()
		self.w.makeKey()
		
	def button_callback(self, sender):
		print("Button click")


Demo()

Any idea what is going on, or what I can do to circumvent this? Many thanks!

I believe the issue is that the buttons need to be stored somewhere. For example, declare a button as

self.button = Button(…)

and then add it as

views=[
    dict(view=self.button)
],

Or use a set/list to keep track of multiple buttons. Here is the fixed example:

class Demo:
	def __init__(self):
		from vanilla import Group, Button, VerticalStackView, FloatingWindow
		self.w = FloatingWindow((1, 1))
		
		metrics = {
			"margin": 10
		}
		self.buttons = set()
		for number in ["one", "two"]:
			group = Group("auto")
			
			button1 = Button("auto", "Button", callback=self.button_callback)
			self.buttons.add(button1)
		
			group.verticalStack = VerticalStackView("auto",
				views=[
					dict(view=button1),
				],
				spacing=10,
				edgeInsets=(10, 10, 10, 10),
			)
			
			group_rules = [
				"H:|-margin-[verticalStack]-margin-|",
				"V:|-margin-[verticalStack]-margin-|",
			]
			
			group.addAutoPosSizeRules(group_rules, metrics)
			
			setattr(self.w, number, group)
			
		rules = [
			"H:|[one]|",
			"H:|[two]|",
			"V:|[one][two]|",
		]
		
		self.w.addAutoPosSizeRules(rules, metrics)
		
		self.w.open()
		self.w.makeKey()
		
	def button_callback(self, sender):
		print("Button click")


Demo()

Disclaimer: I have never used Vanilla before, so I am learning just as much right now.

Thanks! This is indeed the problem. Sadly, this only solves my issue to an extent, as only the button added last is linked to the given callback. So if I instead use

for number in ["one", "two"]:
	group = Group("auto")
	
	self.button1 = Button("auto", "Button", callback=self.button_callback)

only the second button triggers the callback. Can you think of a method to accomodate this?

Have a look at the example above where I store a set buttons in self and then add each button in the loop to that set.

You are a true saviour. Especially for someone who has never used vanilla before. That works perfectly, thank you ever so much!