/ html_css_renderer.lua
html_css_renderer.lua
  1  -- File: html_css_renderer.lua
  2  
  3  --[[
  4  Copyright (C) 2024 chmod777
  5  
  6  This program is free software: you can redistribute it and/or modify it under
  7  the terms of the GNU Affero General Public License version 3 as published by the
  8  Free Software Foundation.
  9  
 10  This program is distributed in the hope that it will be useful, but WITHOUT ANY
 11  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 12  PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
 13  
 14  You should have received a copy of the GNU Affero General Public License along
 15  with this program. If not, see <https://www.gnu.org/licenses/>. 
 16  ]]
 17  
 18  function widget:GetInfo()
 19  	return {
 20  		name    = 'html+css renderer',
 21  		desc    = '',
 22  		author  = 'chmod777',
 23  		date    = 'May 2024',
 24  		license = 'GNU AGPL v3',
 25  		layer   = -999,
 26  		enabled = false,
 27  	}
 28  end
 29  
 30  local luaWidgetDir = 'LuaUI/Widgets/'
 31  
 32  local luaIncludeDir = luaWidgetDir..'Include/'
 33  local LuaShader = VFS.Include(luaIncludeDir..'LuaShader.lua')
 34  
 35  local myIncludesDir = luaWidgetDir..'chmod777_includes/'
 36  local HTMLParser = VFS.Include(myIncludesDir..'html_css/html_parser.lua')
 37  local parse_css = VFS.Include(myIncludesDir..'html_css/parse_css.lua')
 38  local Layout = VFS.Include(myIncludesDir..'html_css/layout.lua')
 39  local Quad,FBO = VFS.Include(myIncludesDir..'utilities_GL4.lua')
 40  
 41  local css_source = VFS.LoadFile(myIncludesDir..'ui/main.css', VFS.RAW)
 42  local html_source = VFS.LoadFile(myIncludesDir..'ui/index.html', VFS.RAW)
 43  
 44  local vsSrc = VFS.LoadFile(myIncludesDir..'shaders/html.vs.glsl', VFS.RAW)
 45  local fsSrc = VFS.LoadFile(myIncludesDir..'shaders/html.fs.glsl', VFS.RAW)
 46  
 47  local InstanceQuad = {}
 48  function InstanceQuad:new(maxElements, maxTexturesPerGroup)
 49  	if maxElements == nil then maxElements = 32 end
 50  	if maxTexturesPerGroup == nil then maxTexturesPerGroup = 8 end
 51  
 52  	local this = {}
 53  	this.elementCount = 0
 54  	this.quad = Quad:new(0, 0, 1, 1, true);
 55  	this.textures = {}
 56  	this.textureGroups = {
 57  		{
 58  			textures = {},
 59  			start = 0,
 60  			count = 0,
 61  		}
 62  	}
 63  	this.maxTexturesPerGroup = maxTexturesPerGroup
 64  
 65  	local instanceVBOLayout = {
 66  		{id = 2, name = 'flags', size = 4, type = GL.INT},
 67  		{id = 3, name = 'position_size', size = 4, type = GL.FLAOT},
 68  		{id = 4, name = 'background_color', size = 4, type = GL.FLAOT},
 69  		{id = 5, name = 'background_images', size = 4, type = GL.INT},
 70  		{id = 6, name = 'border_widths', size = 4, type = GL.FLAOT},
 71  		{id = 7, name = 'border_color', size = 4, type = GL.FLAOT},
 72  		{id = 8, name = 'image0_size_offset', size = 4, type = GL.FLAOT},
 73  		{id = 9, name = 'image1_size_offset', size = 4, type = GL.FLAOT},
 74  		{id = 10, name = 'image2_size_offset', size = 4, type = GL.FLAOT},
 75  		{id = 11, name = 'image3_size_offset', size = 4, type = GL.FLAOT},
 76  		{id = 12, name = 'image0_origin_repeat_image1_origin_repeat', size = 4, type = GL.INT},
 77  		{id = 13, name = 'image2_origin_repeat_image3_origin_repeat', size = 4, type = GL.INT},
 78  		{id = 14, name = 'image_blend_modes', size = 4, type = GL.INT},
 79  	}
 80  	this.instanceVBO = gl.GetVBO(GL.ARRAY_BUFFER, false)
 81  	this.instanceVBO:Define(maxElements, instanceVBOLayout)
 82  	this.quad.VAO:AttachInstanceBuffer(this.instanceVBO)
 83  
 84  	function this:addQuad(data)
 85  		this.instanceVBO:Upload(
 86  			data,
 87  			-1,                  -- attributeIndex
 88  			this.elementCount,   -- elemOffset
 89  			1,                   -- luaStartIndex
 90  			#data                -- luaFinishIndex
 91  		)
 92  		this.textureGroups[#this.textureGroups].count = this.textureGroups[#this.textureGroups].count + 1
 93  		this.elementCount = this.elementCount+1
 94  		return this.elementCount
 95  	end
 96  	function this:doTexturesFitInCurrentGroup(textures)
 97  		local currentGroup = this.textureGroups[#this.textureGroups]
 98  		local indices = {}
 99  		local found_count = 0
100  		for n,new_texture in pairs(textures) do
101  			local found_in_current = false
102  			for c,current_texture in pairs(currentGroup.textures) do
103  				if new_texture == current_texture then
104  					indices[#indices+1] = {n, c}
105  					Spring.Echo()
106  					found_in_current = true
107  					found_count = found_count+1
108  					break
109  				end
110  			end
111  			if not found_in_current then
112  				indices[#indices+1] = {n, #currentGroup.textures+n}
113  			end
114  		end
115  
116  		return (#currentGroup.textures + #textures - found_count) <= this.maxTexturesPerGroup, indices
117  	end
118  	function this:addTexturedQuad(data, textures)
119  		local doTheyFit, indices = this:doTexturesFitInCurrentGroup(textures)
120  		if doTheyFit then
121  			local currentGroup = this.textureGroups[#this.textureGroups]
122  			local data_index = 13
123  			for i=1, #indices do
124  				local n,c = unpack(indices[i])
125  				-- Spring.Echo('n', n, textures[n], 'c', c)
126  				currentGroup.textures[c] = textures[n]
127  				data[data_index] = c
128  				data_index = data_index+1
129  			end
130  			this:addQuad(data)
131  		else
132  			currentGroup = {
133  				textures = {},
134  				start = this.elementCount,
135  				count = 0,
136  			}
137  			this.textureGroups[#this.textureGroups+1] = currentGroup
138  			local data_index = 13
139  			for i=1, #indices do
140  				local n,c = unpack(indices[i])
141  				Spring.Echo('n', n, textures[n], 'c', c)
142  				currentGroup.textures[i] = textures[i]
143  				data[data_index] = i
144  			end
145  			this:addQuad(data)
146  		end
147  	end
148  	function this:Delete()
149  		if this.instanceVBO ~= nil then
150  			this.instanceVBO:Delete()
151  		end
152  		if this.quad ~= nil then
153  			this.quad.Delete()
154  		end
155  	end
156  
157  	return this
158  end
159  
160  local stylesheet
161  local html
162  local style_tree
163  local layout_tree
164  
165  local render_quads
166  local htmlShader
167  local viewGeometryUniformLoc
168  
169  local viewGeometryX,viewGeometryY
170  
171  local build_render_quads
172  local add_quad_recursive
173  local build_shaders
174  
175  function widget:Initialize()
176  	stylesheet = parse_css(css_source)
177  	-- local html_source = '<div/>'
178  	local html_parser = HTMLParser:new(html_source)
179  
180  	-- for i=1, #html_parser.lexemes do
181  	-- 	lexeme = html_parser.lexemes[i]
182  	-- 	Spring.Echo(lexeme.type, lexeme.source)
183  	-- end
184  
185  	document = html_parser.parse()
186  	style_tree = document:build_style_tree(stylesheet)
187  
188  	viewGeometryX,viewGeometryY = Spring.GetViewGeometry()
189  	local size = Layout.Rect:new(0,0,viewGeometryX,viewGeometryY)
190  	layout_tree = Layout.layout_tree(style_tree, Layout.Dimensions:new(size))
191  
192  	build_render_quads(layout_tree)
193  
194  	build_shaders()
195  
196  	if math.bit_or then
197  		Spring.Echo('bit_or found')
198  	else
199  		Spring.Echo('bit_or not found')
200  	end
201  end
202  
203  function widget:DrawScreen()
204  	viewGeometryX,viewGeometryY = Spring.GetViewGeometry()
205  	htmlShader:Activate()
206  		gl.DepthTest(false)
207  		for t,group in pairs(render_quads.textureGroups) do
208  			for i=1, #group.textures do
209  				gl.Texture(i-1, group.textures[i])
210  			end
211  			gl.Uniform(viewGeometryUniformLoc, viewGeometryX, viewGeometryY)
212  			render_quads.quad.VAO:DrawElements(GL.TRIANGLES, 6, 0, group.count, 0, group.start)
213  			for i=1, #group.textures do
214  				gl.Texture(i-1, false)
215  			end
216  		end
217  	htmlShader:Deactivate()
218  end
219  
220  build_render_quads = function(layout_tree)
221  	render_quads = InstanceQuad:new()
222  	add_quad_recursive(layout_tree)
223  end
224  add_quad_recursive = function(layout)
225  	local content = layout.dimensions.content
226  	local x,y,w,h = content.x, content.y, content.width, content.height
227  	y = viewGeometryY-y-h
228  	
229  
230  	local blend_mode = layout.style_node:get_value_or('background-blend-mode', 0)
231  
232  	local background_color = layout.style_node:get_value_or('background-color', Color:new(0,0,0,0,1))
233  	local r,g,b,a = background_color.r,background_color.g,background_color.b,background_color.a
234  
235  	local MAX_BACKGROUND_IMAGES = 4
236  	local background_image_property = layout.style_node:get_declaration('background-image')
237  	local images = {}
238  	if background_image_property ~= nil and background_image_property.value ~= nil then
239  		local image_sources = background_image_property.value
240  		for i,image_source in ipairs(image_sources) do
241  			if i >= MAX_BACKGROUND_IMAGES then break end
242  			images[#images+1] = image_source.value
243  			local texInfo = gl.TextureInfo(image_source.value)
244  			local xsize,ysize = texInfo.xsize,texInfo.ysize
245  			Spring.Echo(image_source.value, xsize, ysize)
246  		end
247  	end
248  
249  	local radius = 16
250  	-- border widths
251  	local btop,bright,bleft,bbot = 5,5,5,5
252  	-- border color
253  	local br,bg,bb,ba = 1,0,0,1
254  
255  
256  	local repeat_mode = 1 + 128 -- x: repeat, y: no-repeat
257  
258  	local data = {
259  		0,0,0,0,                -- unused flags
260  		x,y,w,h,                -- position size
261  		r,g,b,a,                -- background_color
262  		0,0,0,0,                -- background images indices
263  		btop,bright,bleft,bbot, -- border widths
264  		br,bg,bb,ba,            -- border color
265  		256,256,0,0,            -- image0: size xy, offset xy
266  		0,0,0,0,                -- image1: size xy, offset xy
267  		0,0,0,0,                -- image2: size xy, offset xy
268  		0,0,0,0,                -- image3: size xy, offset xy
269  		0,repeat_mode,0,0,                -- image0: origin, repeat mode image1 origin, repeat mode
270  		0,0,0,0,                -- image2: origin, repeat mode image3 origin, repeat mode
271  		0,0,0,0,                -- image0-3: blendmode0, blendmode1, blendmode2, blendmode3
272  	}
273  	if #images > 0 then
274  		render_quads:addTexturedQuad(data, images)
275  	else
276  		render_quads:addQuad(data)
277  	end
278  
279  	for i,child in ipairs(layout.children) do
280  		add_quad_recursive(child)
281  	end
282  end
283  
284  build_shaders = function()
285  	htmlShader = LuaShader({
286  		vertex = vsSrc,
287  		fragment = fsSrc,
288  		uniformFloat = {},
289  		uniformInt = {},
290  		textures = {}
291  	}, 'Quad Shader')
292  	local htmlShaderCompiled = htmlShader:Initialize()
293  	if not htmlShaderCompiled then
294  		Spring.Echo('Quad Shader: compilation failed')
295  		widgetHandler:RemoveWidget()
296  	end
297  	local shader = htmlShader.shaderObj
298  	viewGeometryUniformLoc = gl.GetUniformLocation(shader, 'viewGeometry')
299  end