Skip to content

[NO SQUASH] Create new UI API (Formspec/HUD replacement)#14263

Closed
v-rob wants to merge 7 commits into
luanti-org:masterfrom
v-rob:what-fun
Closed

[NO SQUASH] Create new UI API (Formspec/HUD replacement)#14263
v-rob wants to merge 7 commits into
luanti-org:masterfrom
v-rob:what-fun

Conversation

@v-rob

@v-rob v-rob commented Jan 15, 2024

Copy link
Copy Markdown
Contributor

Closes #6527. Continued from #12926, but reworked stuff like serialization, added full styling support, and made a Lua API for the UI.

This PR is officially Ready for Review undergoing some changes right now.

DO NOT SQUASH. The code is already in nice logical commits.

image

@v-rob v-rob mentioned this pull request Jan 15, 2024
14 tasks
@v-rob v-rob added @ Script API @ Client / Audiovisuals Roadmap The change matches an item on the current roadmap Feature ✨ PRs that add or enhance a feature Formspec labels Jan 15, 2024
@v-rob v-rob force-pushed the what-fun branch 7 times, most recently from 7968d0b to e3a4f26 Compare January 15, 2024 03:49
Comment thread doc/lua_api.md Outdated
@v-rob

v-rob commented Jan 17, 2024

Copy link
Copy Markdown
Contributor Author

Sizers were easier than I'd hoped, so I added them in. There's a flex and grid sizer, bearing no little resemblance to CSS grid and flexbox, albeit somewhat simplified. The documentation also has pretty ASCII images to document how layout works in more detail.

There's also a few other minor changes based on preliminary feedback.

Simple wrapping flex:
image

Simple grid:
image

Code
ui.set_default_theme(ui.Style{
	ui.Style{
		sel = "#root",
	},
	ui.Style{
		sel = "#base",

		rel_pos = {0.5, 0.5},
		rel_anchor = {0.5, 0.5},
		rel_size = {0, 0},

		bg_fill = "black#8C",
	},
})

local function flex_builder(id, player, cx)
	return ui.Window{
		type = "gui",

		style = ui.Style{
			noclip = true,

			ui.Style{
				sel = ".green",

				size = {100, 100},
				bg_fill = "green#8C",
			},
			ui.Style{
				sel = ".blue",

				size = {150, 100},
				bg_fill = "blue#8C",
			},
			ui.Style{
				sel = ".purple",

				size = {100, 120},
				bg_fill = "purple#8C",
			},
			ui.Style{
				sel = ".red",

				size = {100, 100},
				bg_fill = "red#8C",
				weight = 1,
			},
			ui.Style{
				sel = ".cyan",

				size = {100, 100},
				bg_fill = "cyan#8C",
				weight = 2,
			},
			ui.Style{
				sel = ".white",

				size = {700, 100},
				bg_fill = "white#8C",
			},
			ui.Style{
				sel = ".black",

				size = {100, 300},
				bg_fill = "black#8C",
			},
			ui.Style{
				sel = ".sneaky",

				size = {0, 0},
			},
		},

		root = ui.Elem{
			id = "root",

			ui.Flex{
				id = "base",
				dir = "right",
				wrap = "forward",

				--hspacing = "around",
				--vspacing = "remove",

				rel_size = {0.5, 0.9},

				padding = {5, 5, 5, 5},
				gap = {5, 10},

				ui.Elem{groups = {"green"}},
				ui.Elem{groups = {"red"}},
				ui.Elem{groups = {"sneaky"}},
				ui.Elem{groups = {"cyan"}},
				ui.Elem{groups = {"purple"}},
				ui.Elem{groups = {"white"}},
				ui.Elem{groups = {"green"}},
				ui.Elem{groups = {"red"}},
				ui.Elem{groups = {"blue"}},
				ui.Elem{groups = {"red"}},
				ui.Elem{groups = {"cyan"}},
				ui.Elem{groups = {"black"}},
				ui.Elem{groups = {"purple"}},

				--[[
				id = "base",
				dir = "down",
				wrap = "forward",

				hspacing = "remove",
				vspacing = "evenly",

				rel_size = {0.5, 0.9},

				padding = {5, 5, 5, 5},
				gap = {10, 5},

				ui.Elem{groups = {"black"}},
				ui.Elem{groups = {"green"}},
				ui.Elem{groups = {"blue"}},
				ui.Elem{groups = {"blue"}},
				ui.Elem{groups = {"purple"}},
				]]
			},
		},
	}
end

local function grid_builder(id, player, cx)
	return ui.Window{
		type = "gui",

		style = ui.Style{
			noclip = true,

			ui.Style{
				sel = ".green",

				size = {100, 100},
				bg_fill = "green#8C",
			},
			ui.Style{
				sel = ".blue",

				size = {150, 100},
				bg_fill = "blue#8C",
			},
			ui.Style{
				sel = ".purple",

				size = {100, 120},
				bg_fill = "purple#8C",
			},
			ui.Style{
				sel = ".white",

				size = {600, 100},
				bg_fill = "white#8C",
			},
			ui.Style{
				sel = ".black",

				size = {100, 300},
				bg_fill = "black#8C",
			},
			ui.Style{
				sel = ".sneaky",

				size = {0, 0},
			},
		},

		root = ui.Elem{
			id = "root",

			ui.Grid{
				id = "base",

				hsizes = {100, 0, 150},
				vsizes = {100, 120},
				hweights = {0, 1, 0},
				--vweights = {1, 5},

				--hspacing = "around",
				vspacing = "remove",

				rel_size = {0.5, 0.9},

				padding = {5, 5, 5, 5},
				gap = {5, 10},

				ui.Elem{groups = {"white"},  pos = {0, 0}, span = {3, 1}},
				ui.Elem{groups = {"green"},  pos = {1, 1}, span = {1, 1}},
				ui.Elem{groups = {"purple"}, pos = {0, 1}, span = {1, 1}},
				ui.Elem{groups = {"blue"},   pos = {2, 1}, span = {1, 1}},
				ui.Elem{groups = {"black"},  pos = {1, 2}, span = {1, 1}},
				ui.Elem{groups = {"sneaky"}, pos = {0, 0}, span = {3, 3}},
			},
		},
	}
end

core.register_on_joinplayer(function(player)
	local id = ui.open(flex_builder, player:get_player_name())
end)

@v-rob v-rob force-pushed the what-fun branch 3 times, most recently from babe696 to b2f28c9 Compare January 19, 2024 07:31
@v-rob

v-rob commented Jan 19, 2024

Copy link
Copy Markdown
Contributor Author

I beefed up style selectors in power, so they have things similar to child/parent selectors and stuff now. In some respects, they have a few features that are more convenient than CSS itself, such as the ability to group things like (button, checkbox)($hovered, $pressed) instead of the more verbose button$hovered, button$pressed, checkbox$hovered, checkbox$pressed). It still beats CSS for simplicity, though, which is definitely desirable.

As usual, here's some example code and a screenshot of the output (nothing amazing to look at, but shows the expected output):

Code
ui.set_default_theme(ui.Style{
	ui.Style{
		sel = "?root",
	},
	ui.Style{
		sel = "?<(?root)",

		rel_pos = {0.5, 0.5},
		rel_anchor = {0.5, 0.5},
		rel_size = {0, 0},

		bg_fill = "black#8C",
	},
})

local function style_builder(id, player, cx)
	return ui.Window{
		type = "gui",

		style = ui.Style{
			ui.Style{
				sel = "?<(flex)",

				size = {100, 100},
				bg_fill = "green#8C",
			},
			ui.Style{
				sel = "flex",

				fg_image = "cdb_queued.png",
				fg_scale = 1,

				ui.Style{
					sel = "?horizontal",

					fg_valign = "bottom",
				},
				ui.Style{
					sel = "?vertical",

					fg_halign = "right",
				},
			},
			ui.Style{
				sel = ".blue?<(flex)",

				bg_fill = "blue#8C",
			},
			ui.Style{
				sel = "flex!(/hud/, elem, #that)",

				bg_fill = "white#8C",
			},
			ui.Style{
				sel = "?nth_child(3)?nth_last_child(6)",

				bg_fill = "purple#8C",
			},
			ui.Style{
				sel = "flex?>(.blue)",

				rel_anchor = {0.4, 0.4},
			},
			ui.Style{
				sel = "?root?>>(.blue)",

				fg_image = "air.png",
				fg_scale = 1,
			},
			ui.Style{
				sel = ".what?<<(#bob)",

				bg_fill = "red#8C",
			},
			ui.Style{
				sel = ".eh?<>(.what)",

				bg_fill = "black#8C",
			},
			ui.Style{
				sel = ".maybe_empty?empty",

				bg_fill = "yellow#8C",
			},
			ui.Style{
				sel = "(?root, ?<(.maybe_empty))?only_child",

				fg_fill = "blue#3",
				fg_halign = "right",
			},
			ui.Style{
				sel = "(?first_child, ?last_child)?<(flex)",

				fg_image = "cdb_add.png",
			},
		},

		root = ui.Elem{
			id = "bob",
			groups = {"blue"},

			ui.Flex{
				id = "base",

				dir = "down",
				wrap = "forward",

				rel_size = {0.5, 0.5},

				padding = {5, 5, 5, 5},
				gap = {5, 5},

				ui.Elem{},
				ui.Elem{groups = {"blue"}},
				ui.Elem{},
				ui.Elem{groups = {"what", "eh"}},
				ui.Elem{groups = {"eh"}},
				ui.Elem{groups = {"maybe_empty"}},
				ui.Elem{
					groups = {"maybe_empty"},
					ui.Elem{
						fg_image = "cdb_update.png",
						fg_scale = 1,
					},
				},
				ui.Elem{},
			}
		},
	}
end

core.register_on_joinplayer(function(player)
	local id = ui.open(style_builder, player:get_player_name())
	core.after(2, function() ui.update(id) end)
end)

image

@v-rob v-rob force-pushed the what-fun branch 10 times, most recently from 6e94043 to 40c1f65 Compare January 24, 2024 08:35
@NyxousLev01

Copy link
Copy Markdown

How about also a Support for Copying Text or Selecting Texts on Touchscreen primarily in Android or iOS, Formspec doesn't really have that feature, its not fair PCs primarily have that while on Touchscreen Devices it doesn't.

@v-rob

v-rob commented Jun 14, 2025

Copy link
Copy Markdown
Contributor Author

How about also a Support for Copying Text or Selecting Texts on Touchscreen primarily in Android or iOS, Formspec doesn't really have that feature, its not fair PCs primarily have that while on Touchscreen Devices it doesn't.

This PR doesn't have any support for text editing yet; that will come in future PRs.

@Zughy

Zughy commented Jun 14, 2025

Copy link
Copy Markdown
Contributor

@v-rob does the action/change needed label still apply?

@v-rob

v-rob commented Jun 15, 2025

Copy link
Copy Markdown
Contributor Author

@v-rob does the action/change needed label still apply?

Yes for now. A few open review comments exist that I am still in the process of addressing.

@rdnuk

rdnuk commented Aug 7, 2025

Copy link
Copy Markdown

Looking forward to this, is it still being worked on?

@Zughy

Zughy commented Sep 15, 2025

Copy link
Copy Markdown
Contributor

@v-rob rebase needed (and also comments to address)

@Zughy Zughy added the Rebase needed The PR needs to be rebased by its author label Sep 15, 2025
@alwayshopeless

alwayshopeless commented Oct 15, 2025

Copy link
Copy Markdown

A very interesting PR. Flexbox/grid-equivalent functionality, and explicit parameter names are very useful.

If you plan to continue work with this PR, I'd be very happy to see the Canvas API, which you demonstrated in the old closed PR. Combined with keyboard input capture for the UI, this would allow for interactive maps, rich inventories, minigames, or even writing your own UI framework using the Canvas API (if you implement deferred input, as it currently works with VoxelManip).

Thank you for your work!

@v-rob v-rob removed the Rebase needed The PR needs to be rebased by its author label Nov 15, 2025
@Zughy

Zughy commented Dec 21, 2025

Copy link
Copy Markdown
Contributor

@v-rob rebase needed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Adoption needed The pull request needs someone to adopt it. Adoption welcomed! @ Client / Audiovisuals Feature ✨ PRs that add or enhance a feature Formspec Rebase needed The PR needs to be rebased by its author Roadmap The change matches an item on the current roadmap @ Script API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Formspec replacement