-- lx-bundle:true

-- This file is part of Lx and Qui, created by Liqube <www.liqube.com>
-- Copyright Liqube Audio. All rights reserved.
-- Lx Interim License. See LICENSE for details.

--- Provide a virtual Tree that requests only rows needed for the current viewport
-- @module qui.vtree

local Tree = require("qui.tree")
local glyphs = require("qui.glyph")
local theme = require("qui.theme")

local VirtualTree = {}
local theme_generation = 0	-- Separates retained presentations made under different active Themes

-- One framework listener invalidates every VirtualTree presentation without retaining individual Controls
theme.on_apply(function()
	theme_generation = theme_generation + 1

	return true
end)

local DrawingRecorder = {}
DrawingRecorder.__index = DrawingRecorder

--- Return the callback-scoped Canvas or reject a recorder retained by application code
-- @return userdata control_canvas
function DrawingRecorder:active_canvas()
	assert(self.canvas ~= nil, "VirtualTree cell Canvas is only valid during its drawing callback")

	return self.canvas
end

--- Retain one drawing command relative to its cell so scrolling can replay it elsewhere
-- @param name string control Canvas method name
-- @param values table ordered method arguments
function DrawingRecorder:append(name, values)
	self:active_canvas()
	values.name = name
	self.commands[#self.commands + 1] = values
end

--- Report the real Control size to custom drawing code
-- @return integer width
-- @return integer height
function DrawingRecorder:size()
	return self:active_canvas():size()
end

--- Retain one filled rectangle
function DrawingRecorder:fill_rect(x, y, width, height, color)
	self:append("fill_rect", { x - self.x, y - self.y, width, height, color })
end

--- Retain one filled circle
function DrawingRecorder:fill_circle(x, y, diameter, color)
	self:append("fill_circle", { x - self.x, y - self.y, diameter, color })
end

--- Retain one circle outline
function DrawingRecorder:draw_circle(x, y, diameter, frame_width, color)
	self:append("draw_circle", { x - self.x, y - self.y, diameter, frame_width, color })
end

--- Retain one regular rectangle outline
function DrawingRecorder:draw_rect(x, y, width, height, frame_width, color)
	self:append("draw_rect", { x - self.x, y - self.y, width, height, frame_width, color })
end

--- Retain one horizontal line
function DrawingRecorder:draw_horizontal_line(x, y, length, width, color)
	self:append("draw_horizontal_line", { x - self.x, y - self.y, length, width, color })
end

--- Retain one QUI frame fill
function DrawingRecorder:fill_frame(x, y, width, height, color)
	self:append("fill_frame", { x - self.x, y - self.y, width, height, color })
end

--- Retain one QUI frame outline
function DrawingRecorder:draw_frame(x, y, width, height, frame_width, color)
	self:append("draw_frame", { x - self.x, y - self.y, width, height, frame_width, color })
end

--- Retain one named UI glyph
function DrawingRecorder:draw_glyph(name, x, y, width, height, color)
	self:append("draw_glyph", { name, x - self.x, y - self.y, width, height, color })
end

--- Measure through the live Canvas because measurement itself adds no drawing command
function DrawingRecorder:measure_text(font, text)
	return self:active_canvas():measure_text(font, text)
end

--- Resolve a shaped text position through the live Canvas
function DrawingRecorder:text_x(font, text, column)
	return self:active_canvas():text_x(font, text, column)
end

--- Resolve a shaped text column through the live Canvas
function DrawingRecorder:text_column(font, text, x)
	return self:active_canvas():text_column(font, text, x)
end

--- Resolve shaped text clusters through the live Canvas
function DrawingRecorder:text_clusters(font, text)
	return self:active_canvas():text_clusters(font, text)
end

--- Retain shaped text and preserve the ordinary draw_text return value
function DrawingRecorder:draw_text(font, text, x, y, color)
	self:append("draw_text", { font, text, x - self.x, y - self.y, color })
	local width = self:active_canvas():measure_text(font, text)

	return x + width
end

--- Finish one recording and revoke its callback-scoped Canvas
-- @return table commands
function DrawingRecorder:finish()
	local commands = self.commands
	self.canvas = nil

	return commands
end

--- Run one dirty-cell drawing handler and always revoke its temporary Canvas recorder
-- @param control_canvas userdata active drawing surface
-- @param x integer cell left
-- @param y integer cell top
-- @param handler function custom cell drawing handler
-- @param owner table VirtualTree control
-- @param context table current cell description
-- @param style table retained cell style
-- @return table commands
local function record_drawing(control_canvas, x, y, handler, owner, context, style)
	local recorder = setmetatable({
		canvas = control_canvas,
		x = x,
		y = y,
		commands = {},
	}, DrawingRecorder)
	local succeeded, failure = pcall(handler, owner, recorder, context, style)
	local commands = recorder:finish()
	if not succeeded then
		error(failure, 0)
	end

	return commands
end

--- Replay retained commands through the ordinary protected Canvas
-- @param control_canvas userdata active drawing surface
-- @param commands table retained drawing commands
-- @param x integer current cell left
-- @param y integer current cell top
local function replay_drawing(control_canvas, commands, x, y)
	for _, command in ipairs(commands) do
		local name = command.name
		if name == "fill_rect" then
			control_canvas:fill_rect(x + command[1], y + command[2], command[3], command[4], command[5])
		elseif name == "fill_circle" then
			control_canvas:fill_circle(x + command[1], y + command[2], command[3], command[4])
		elseif name == "draw_circle" then
			control_canvas:draw_circle(
				x + command[1], y + command[2], command[3], command[4], command[5])
		elseif name == "draw_rect" then
			control_canvas:draw_rect(
				x + command[1], y + command[2], command[3], command[4], command[5], command[6])
		elseif name == "draw_horizontal_line" then
			control_canvas:draw_horizontal_line(
				x + command[1], y + command[2], command[3], command[4], command[5])
		elseif name == "fill_frame" then
			control_canvas:fill_frame(x + command[1], y + command[2], command[3], command[4], command[5])
		elseif name == "draw_frame" then
			control_canvas:draw_frame(
				x + command[1], y + command[2], command[3], command[4], command[5], command[6])
		elseif name == "draw_glyph" then
			control_canvas:draw_glyph(
				command[1], x + command[2], y + command[3], command[4], command[5], command[6])
		elseif name == "draw_text" then
			control_canvas:draw_text(
				command[1], command[2], x + command[3], y + command[4], command[5])
		else
			error("VirtualTree retained an unknown drawing command " .. tostring(name))
		end
	end
end

--- Pack the cell conditions visible to styling and drawing handlers into one small cache key
-- @param context table current cell description
-- @return integer key
local function cell_state_key(context)
	local key = 0
	if context.selected then
		key = key + 1
	end
	if context.hovering then
		key = key + 2
	end
	if context.focused then
		key = key + 4
	end
	if context.enabled then
		key = key + 8
	end

	return key
end

--- Confirm that a provider count is a safe whole number
-- @param value any returned count
-- @param name string readable callback name
-- @return integer count
local function checked_count(value, name)
	assert(type(value) == "number" and value >= 0 and value < math.huge and value % 1 == 0,
		name .. " must return a non-negative whole number")

	return value
end

--- Confirm that one caller table is a gapless list
-- @param source table caller-supplied list
-- @param name string readable option name
local function check_list(source, name)
	local count = 0
	local highest = 0
	for key in pairs(source) do
		assert(type(key) == "number" and key >= 1 and key % 1 == 0,
			name .. " must use positive whole-number positions")
		count = count + 1
		highest = math.max(highest, key)
	end
	assert(highest == count, name .. " must be a gapless list")
end

--- Read one fixed DIP or percentage column width
-- @param value number|string|nil requested width
-- @param name string readable option name
-- @return string kind
-- @return number amount
local function column_width(value, name)
	if value == nil then
		return "automatic", 0
	end
	if type(value) == "number" then
		assert(value >= 0 and value < math.huge, name .. " must be a non-negative DIP number")

		return "dip", math.floor(value + 0.5)
	end
	assert(type(value) == "string" and value:sub(-1) == "%",
		name .. " must be a DIP number or percentage ending in '%'")
	local amount = tonumber(value:sub(1, -2))
	assert(amount and amount >= 0 and amount < math.huge, name .. " contains an invalid percentage")

	return "percentage", amount / 100
end

--- Copy stable column declarations without retaining caller-owned tables
-- @param source table|nil caller columns
-- @param main_name string|nil requested remaining-space column
-- @return table|nil columns
-- @return table|nil columns_by_name
-- @return table|nil main_column
-- @return table|nil tree_column
local function prepare_columns(source, main_name)
	if source == nil then
		return nil, nil, nil, nil
	end
	assert(type(source) == "table", "VirtualTree columns must be a table")
	check_list(source, "VirtualTree columns")
	assert(#source > 0, "VirtualTree columns must not be empty")

	local columns = {}
	local columns_by_name = {}
	for index, declaration in ipairs(source) do
		local path = "VirtualTree columns[" .. index .. "]"
		assert(type(declaration) == "table", path .. " must be a table")
		assert(type(declaration.name) == "string" and declaration.name ~= "", path .. ".name must be a string")
		assert(columns_by_name[declaration.name] == nil, "VirtualTree column names must be unique")
		assert(type(declaration.caption) == "string", path .. ".caption must be a string")
		assert(declaration.align == nil or declaration.align == "left" or
			declaration.align == "center" or declaration.align == "right",
			path .. ".align must be 'left', 'center', or 'right'")
		assert(declaration.visible == nil or type(declaration.visible) == "boolean",
			path .. ".visible must be a boolean")
		assert(declaration.resizable == nil or type(declaration.resizable) == "boolean",
			path .. ".resizable must be a boolean")
		assert(declaration.sortable == nil or type(declaration.sortable) == "boolean",
			path .. ".sortable must be a boolean")
		local width_kind, width = column_width(declaration.width, path .. ".width")
		local minimum = declaration.min_width or 24
		local maximum = declaration.max_width or math.huge
		assert(type(minimum) == "number" and minimum >= 0 and minimum < math.huge,
			path .. ".min_width must be a non-negative DIP number")
		assert(type(maximum) == "number" and maximum >= minimum and maximum <= math.huge,
			path .. ".max_width must be at least min_width")

		local column = {
			name = declaration.name,
			caption = declaration.caption,
			align = declaration.align or "left",
			visible = declaration.visible ~= false,
			resizable = declaration.resizable ~= false,
			sortable = declaration.sortable ~= false,
			width_kind = width_kind,
			width = width,
			default_width_kind = width_kind,	-- Reset returns to the declaration rather than a later live width
			default_width = width,
			min_width = math.floor(minimum + 0.5),
			max_width = maximum == math.huge and maximum or math.floor(maximum + 0.5),
			index = index,
		}
		columns[#columns + 1] = column
		columns_by_name[column.name] = column
	end

	local main_column
	if main_name then
		assert(type(main_name) == "string" and columns_by_name[main_name],
			"VirtualTree main_column must name a known column")
		main_column = columns_by_name[main_name]
	else
		for _, column in ipairs(columns) do
			if column.visible then
				main_column = column
				break
			end
		end
	end
	assert(main_column, "VirtualTree columns must contain a visible column")
	assert(main_column.visible, "VirtualTree main_column must be visible")

	-- Tree structure follows display order and stays independent from Resonic-style main-column filling
	local tree_column
	for _, column in ipairs(columns) do
		if column.visible then
			tree_column = column
			break
		end
	end

	return columns, columns_by_name, main_column, tree_column
end

--- Resolve visible column rectangles and their complete content width
-- Automatic columns share the room left after DIP and percentage columns
-- @param columns table prepared columns
-- @param available integer available row width
-- @param main_column table remaining-space column
-- @param fit_to_width boolean whether the main column should absorb remaining width
-- @return table geometry
-- @return integer content_width
-- @return boolean fit_succeeded
local function arrange_columns(columns, available, main_column, fit_to_width)
	local fixed = 0
	local automatic = 0
	for _, column in ipairs(columns) do
		if column.visible then
			if column.drag_width ~= nil then
				fixed = fixed + column.drag_width
			elseif column.width_kind == "dip" then
				fixed = fixed + column.width
			elseif column.width_kind == "percentage" then
				fixed = fixed + math.floor(available * column.width + 0.5)
			else
				automatic = automatic + 1
			end
		end
	end

	local automatic_width = automatic > 0 and math.max(0, available - fixed) / automatic or 0
	local resolved_widths = {}
	local combined_width = 0
	for _, column in ipairs(columns) do
		if column.visible then
			local width
			if column.drag_width ~= nil then
				width = column.drag_width
			elseif column.width_kind == "dip" then
				width = column.width
			elseif column.width_kind == "percentage" then
				width = math.floor(available * column.width + 0.5)
			else
				width = math.floor(automatic_width + 0.5)
			end
			width = math.max(column.min_width, math.min(width, column.max_width))
			resolved_widths[column] = width
			combined_width = combined_width + width
		end
	end

	local fit_succeeded = true
	if fit_to_width then
		-- The main column alone absorbs spare room and yields until its declared minimum
		local other_width = combined_width - resolved_widths[main_column]
		local remaining = available - other_width
		fit_succeeded = remaining >= main_column.min_width
		resolved_widths[main_column] =
			math.max(main_column.min_width, math.min(remaining, main_column.max_width))
	end

	local geometry = {}
	local x = 1
	for _, column in ipairs(columns) do
		if column.visible then
			local width = resolved_widths[column]
			column.resolved_width = width	-- Live display width remains separate from the declared/reset width
			geometry[#geometry + 1] = {
				column = column,
				x = x,
				width = width,
			}
			x = x + width
		end
	end

	return geometry, math.max(0, x - 1), fit_succeeded
end

--- Shorten one caption at complete shaped-text boundaries so it stays inside its cell
-- @param control_canvas userdata active drawing surface
-- @param font userdata current UI font
-- @param caption string complete caption
-- @param available integer available text width
-- @return string fitted_caption
-- @return integer fitted_width
-- @return integer natural_width
local function fit_caption(control_canvas, font, caption, available)
	available = math.max(0, available)
	local width = control_canvas:measure_text(font, caption)
	if width <= available then
		return caption, width, width
	end

	local ellipsis = "…"
	local ellipsis_width = control_canvas:measure_text(font, ellipsis)
	if ellipsis_width > available then
		return "", 0, width
	end

	local final_column = 1
	for _, cluster in ipairs(control_canvas:text_clusters(font, caption)) do
		if cluster[4] + ellipsis_width > available then
			break
		end
		final_column = cluster[2]
	end
	local fitted = caption:sub(1, final_column - 1) .. ellipsis

	return fitted, control_canvas:measure_text(font, fitted), width
end

--- Insert one expanded branch in direct-child order
-- Only expanded nodes occupy this small list; untouched siblings remain number ranges
-- @param container table root or branch container
-- @param branch table expanded branch
local function insert_branch(container, branch)
	local position = #container.branches + 1
	for index, existing in ipairs(container.branches) do
		if branch.index < existing.index then
			position = index
			break
		end
	end
	table.insert(container.branches, position, branch)
	container.branches_by_index[branch.index] = branch
end

--- Count visible rows below one root or expanded branch without creating its ordinary children
-- @param container table root or branch container
-- @return integer count
local function container_rows(container)
	local count = container.count
	for _, branch in ipairs(container.branches) do
		if branch.expanded then
			count = count + container_rows(branch)
		end
	end

	return count
end

--- Create one virtual Tree for use inside 'qui.pane.controls'
-- Nodes remain owned by the application; QUI keeps only viewport rows and explicitly expanded branches
-- @param options table provider callbacks, sizing, placement, selection, and optional on_change handler
-- @return table control unrestricted Lua Control declaration
function VirtualTree.new(options)
	assert(type(options) == "table", "VirtualTree expects a table")
	assert(options.items == nil, "VirtualTree uses provider callbacks instead of items")
	assert(type(options.root_count) == "function", "VirtualTree root_count must be a function")
	assert(type(options.child_count) == "function", "VirtualTree child_count must be a function")
	assert(type(options.child) == "function", "VirtualTree child must be a function")
	local columns, columns_by_name, main_column, tree_column =
		prepare_columns(options.columns, options.main_column)
	local sort_column
	local sort_direction
	if columns and options.sort_column ~= nil then
		assert(type(options.sort_column) == "string" and columns_by_name[options.sort_column],
			"VirtualTree sort_column must name a known column")
		sort_column = columns_by_name[options.sort_column]
		sort_direction = options.sort_direction or "ascending"
		assert(sort_direction == "ascending" or sort_direction == "descending",
			"VirtualTree sort_direction must be 'ascending' or 'descending'")
	else
		assert(options.sort_column == nil,
			"VirtualTree sort_column requires columns")
		assert(options.sort_direction == nil,
			"VirtualTree sort_direction requires sort_column")
	end
	assert(columns and type(options.cell_text) == "function" or not columns and type(options.text) == "function",
		"VirtualTree requires cell_text with columns, or text without columns")
	assert(options.has_children == nil or type(options.has_children) == "function",
		"VirtualTree has_children must be a function")
	assert(options.node_enabled == nil or type(options.node_enabled) == "function",
		"VirtualTree node_enabled must be a function")
	assert(options.on_expand == nil or type(options.on_expand) == "function",
		"VirtualTree on_expand must be a function")
	assert(options.on_change == nil or type(options.on_change) == "function",
		"VirtualTree on_change must be a function")
	assert(options.on_column_resize == nil or type(options.on_column_resize) == "function",
		"VirtualTree on_column_resize must be a function")
	assert(options.on_sort == nil or type(options.on_sort) == "function",
		"VirtualTree on_sort must be a function")
	assert(options.cell_style == nil or type(options.cell_style) == "function",
		"VirtualTree cell_style must be a function")
	assert(options.draw_cell_background == nil or type(options.draw_cell_background) == "function",
		"VirtualTree draw_cell_background must be a function")
	assert(options.draw_cell_overlay == nil or type(options.draw_cell_overlay) == "function",
		"VirtualTree draw_cell_overlay must be a function")
	assert(columns or
		options.cell_style == nil and options.draw_cell_background == nil and options.draw_cell_overlay == nil,
		"VirtualTree cell drawing callbacks require columns")

	local control = options
	local root_count = control.root_count
	local child_count = control.child_count
	local child = control.child
	local text = control.text
	local cell_text = control.cell_text
	local has_children = control.has_children
	local node_enabled = control.node_enabled
	local on_expand = control.on_expand
	local on_change = control.on_change
	local on_column_resize = control.on_column_resize
	local on_sort = control.on_sort
	local cell_style = control.cell_style
	local draw_cell_background = control.draw_cell_background
	local draw_cell_overlay = control.draw_cell_overlay
	local initial_selection = control.selected
	local header_height = control.header_height or control.row_height or 30
	if columns then
		assert(type(header_height) == "number" and header_height > 0 and header_height < math.huge,
			"VirtualTree header_height must be a positive DIP number")
	end

	--- Read and validate one application-provided cell caption
	-- @param node any application-owned node identity
	-- @param column table prepared column
	-- @return string caption
	local function read_cell(node, column)
		local caption = cell_text(node, column)
		assert(type(caption) == "string", "VirtualTree cell_text must return a string")

		return caption
	end

	--- Read the visible properties of one application-owned node
	-- @param node any application-owned node identity
	-- @return string caption
	-- @return boolean enabled
	-- @return boolean branch
	local function describe(node)
		assert(node ~= nil, "VirtualTree child must return a node")
		local caption = columns and read_cell(node, tree_column) or text(node)
		assert(type(caption) == "string", "VirtualTree text must return a string")
		local enabled = true
		if node_enabled then
			enabled = node_enabled(node)
			assert(type(enabled) == "boolean", "VirtualTree node_enabled must return a boolean")
		end

		local branch
		if has_children then
			branch = has_children(node)
		else
			branch = checked_count(child_count(node), "VirtualTree child_count") > 0
		end
		assert(type(branch) == "boolean", "VirtualTree has_children must return a boolean")

		return caption, enabled, branch
	end

	local state = {
		root = {
			source = nil,
			count = checked_count(root_count(), "VirtualTree root_count"),
			branches = {},
			branches_by_index = {},
		},
		branches_by_source = {},
		rows = {},
		row_order = {},
		source_rows = {},
	}

	--- Forget transient viewport rows while retaining selection and expanded branch state
	function state:clear_rows()
		self.rows = {}
		self.row_order = {}
		self.source_rows = {}
	end

	--- Forget retained presentation while keeping provider text and sparse rows available
	function state:clear_presentations()
		for _, row in pairs(self.source_rows) do
			row.cell_presentations = {}
		end
	end

	--- Retain one recently requested row in the deliberately small viewport cache
	-- @param row table normalized visible row
	function state:cache_row(row)
		self.rows[row.row_index] = row
		self.source_rows[row.source] = row
		self.row_order[#self.row_order + 1] = row
		if #self.row_order <= 64 then
			return
		end

		local expired = table.remove(self.row_order, 1)
		if self.rows[expired.row_index] == expired then
			self.rows[expired.row_index] = nil
		end
		if self.source_rows[expired.source] == expired then
			self.source_rows[expired.source] = nil
		end
	end

	--- Create one lightweight row for a direct position in a sparse container
	-- @param container table root or expanded branch
	-- @param index integer direct child position
	-- @param depth integer visual nesting depth
	-- @param parent table|nil resolved parent row
	-- @param row_index integer visible row position
	-- @return table row
	function state:direct_row(container, index, depth, parent, row_index)
		local branch = container.branches_by_index[index]
		local source = branch and branch.source or child(container.source, index)
		local caption, enabled, branch_available = describe(source)

		local row = {
			source = source,
			text = caption,
			enabled = enabled,
			has_children = branch_available,
			expanded = branch and branch.expanded or false,
			parent = parent,
			depth = depth,
			container = container,
			direct_index = index,
			row_index = row_index,
			branch = branch,
			cells = columns and { [tree_column.name] = caption } or nil,
			cell_contexts = columns and {} or nil,
			cell_presentations = columns and {} or nil,	-- Bounded styles and custom drawing grouped by column
		}
		if branch then
			branch.row = row
		end
		if self.control and self.control.selected_node == source then
			if enabled then
				self.control._selected_node = row
			else
				self.control._selected_node = nil
				self.control.selected_node = nil
				self.control.selected = nil
			end
		end
		self:cache_row(row)

		return row
	end

	--- Resolve one flattened row by skipping untouched sibling ranges as simple counts
	-- @param container table root or expanded branch
	-- @param offset integer one-based row inside this container
	-- @param depth integer visual nesting depth
	-- @param parent table|nil resolved parent row
	-- @param row_index integer absolute visible row position
	-- @return table|nil row
	function state:resolve(container, offset, depth, parent, row_index)
		if offset < 1 or offset > container_rows(container) then
			return nil
		end

		local next_direct = 1
		for _, branch in ipairs(container.branches) do
			if branch.expanded then
				local ordinary_count = branch.index - next_direct
				if offset <= ordinary_count then
					return self:direct_row(container, next_direct + offset - 1, depth, parent, row_index)
				end
				offset = offset - ordinary_count

				local branch_row = self:direct_row(container, branch.index, depth, parent,
					row_index - offset + 1)
				if offset == 1 then
					return branch_row
				end
				offset = offset - 1

				local child_rows = container_rows(branch)
				if offset <= child_rows then
					return self:resolve(branch, offset, depth + 1, branch_row, row_index)
				end
				offset = offset - child_rows
				next_direct = branch.index + 1
			end
		end

		return self:direct_row(container, next_direct + offset - 1, depth, parent, row_index)
	end

	--- Return one visible row without creating the preceding rows
	-- @param index integer one-based visible row
	-- @return table|nil row
	function state:row(index)
		if type(index) ~= "number" or index < 1 or index > container_rows(self.root) then
			return nil
		end
		local cached = self.rows[index]
		if cached then
			return cached
		end

		return self:resolve(self.root, index, 0, nil, index)
	end

	--- Keep one known row inside the current viewport
	-- @param owner table VirtualTree control
	-- @param row_index integer visible row position
	function state:reveal(owner, row_index)
		local visible = owner:_visible_row_count()
		local maximum_first = math.max(1, container_rows(self.root) - visible + 1)
		if row_index < owner.first_visible then
			owner.first_visible = row_index
		elseif row_index >= owner.first_visible + visible then
			owner.first_visible = row_index - visible + 1
		end
		owner.first_visible = math.max(1, math.min(owner.first_visible, maximum_first))
	end

	--- Change one sparse branch while retaining only its compact expansion record
	-- @param owner table VirtualTree control
	-- @param row table materialized branch row
	-- @param expanded boolean requested state
	-- @param invalidate boolean whether live drawing must be requested
	-- @return boolean|nil changed
	-- @return string|nil error_text
	function state:set_expanded(owner, row, expanded, invalidate)
		if not row.has_children or row.expanded == expanded then
			return true
		end

		local branch = row.branch or self.branches_by_source[row.source]
		if not branch then
			if on_expand then
				on_expand(row.source)
			end
			local count = checked_count(child_count(row.source), "VirtualTree child_count")
			if count == 0 then
				row.has_children = false
				return true
			end
			branch = {
				source = row.source,
				index = row.direct_index,
				count = count,
				expanded = false,
				branches = {},
				branches_by_index = {},
				parent_container = row.container,
				row = row,
			}
			insert_branch(row.container, branch)
			self.branches_by_source[row.source] = branch
			row.branch = branch
		end

		local previous_selection = owner._selected_node
		local previous_first = owner.first_visible
		branch.expanded = expanded
		row.expanded = expanded
		if not expanded and previous_selection then
			local ancestor = previous_selection.parent
			while ancestor and ancestor.source ~= row.source do
				ancestor = ancestor.parent
			end
			if ancestor then
				owner._selected_node = row
				owner.selected_node = row.source
				owner.selected = row.source
				self:reveal(owner, row.row_index)
			end
		end
		self:clear_rows()

		if invalidate then
			local invalidated, invalidate_error = owner:redraw()
			if not invalidated then
				branch.expanded = not expanded
				row.expanded = not expanded
				owner._selected_node = previous_selection
				owner.selected_node = previous_selection and previous_selection.source or nil
				owner.selected = owner.selected_node
				owner.first_visible = previous_first
				self:clear_rows()

				return nil, invalidate_error
			end
		end
		if owner._selected_node ~= previous_selection and owner.on_change then
			owner:on_change(owner.selected, owner.selected_node)
		end

		return true
	end

	control.root_count = nil
	control.child_count = nil
	control.child = nil
	control.text = nil
	control.cell_text = nil
	control.has_children = nil
	control.node_enabled = nil
	control.on_expand = nil
	control.on_change = nil
	control.on_column_resize = nil
	control.on_sort = nil
	control.cell_style = nil
	control.draw_cell_background = nil
	control.draw_cell_overlay = nil
	control.selected = nil
	control.items = {}
	control._header_height = columns and math.floor(header_height + 0.5) or nil
	control.header_height = nil
	control.columns = columns
	control.main_column = main_column and main_column.name or nil
	control.sort_column = sort_column and sort_column.name or nil
	control.sort_direction = sort_direction
	if columns then
		local fit_to_width = true	-- Manual main-column sizing or unavoidable overflow suspends fitting
		local header_resize = nil	-- Retains one divider drag until release or cancellation
		local header_press = nil	-- Separates a sortable caption click from divider interaction
		control.fit_to_width = true

		--- Keep the public fit status synchronized with the private layout choice
		-- @param value boolean requested state
		local function store_fit_to_width(value)
			fit_to_width = value
			control.fit_to_width = value
		end

		--- Resolve and retain column geometry shared by overflow checks, headers, and rows
		-- @param self table VirtualTree control
		-- @param available integer visible row width before horizontal scrolling
		-- @return table geometry
		-- @return integer content_width
		function control:_arrange_columns(available)
			if self._column_geometry_available ~= available then
				local fit_succeeded
				self._column_geometry, self._column_content_width, fit_succeeded =
					arrange_columns(columns, available, main_column, fit_to_width)
				if fit_to_width and not fit_succeeded then
					-- An impossible fit becomes an ordinary minimum-width column until explicitly restored
					main_column.width_kind = "dip"
					main_column.width = main_column.min_width
					store_fit_to_width(false)
				end
				self._column_geometry_available = available
			end

			return self._column_geometry, self._column_content_width
		end

		--- Forget only column rectangles after a live layout choice changes
		-- Cell values remain cached because moving a cell does not change its application data
		-- @param self table VirtualTree control
		local function forget_column_geometry(self)
			self._column_geometry = nil
			self._column_content_width = nil
			self._column_geometry_available = nil
			self._visible_column_geometry = nil
			self._visible_column_right = nil
			self._visible_column_offset = nil
		end

		--- Store one explicit live width without requesting work from inside a pointer event
		-- @param self table VirtualTree control
		-- @param column table prepared column
		-- @param width number requested DIP width
		local function store_column_width(self, column, width)
			column.width_kind = "dip"
			column.width = math.max(
				column.min_width,
				math.min(math.floor(width + 0.5), column.max_width)
			)
			if column == main_column then
				store_fit_to_width(false)
			end
			forget_column_geometry(self)
		end

		--- Set one live column width in DIPs
		-- Manually sizing the main column suspends filling until that column is reset
		-- @param self table VirtualTree control
		-- @param column_name string stable column name
		-- @param width number requested DIP width
		-- @return boolean|nil changed
		-- @return string|nil error_text
		function control:set_column_width(column_name, width)
			assert(type(column_name) == "string" and columns_by_name[column_name],
				"VirtualTree set_column_width requires a known column name")
			assert(type(width) == "number" and width >= 0 and width < math.huge,
				"VirtualTree set_column_width requires a non-negative DIP width")
			local column = columns_by_name[column_name]
			store_column_width(self, column, width)

			return self:relayout()
		end

		--- Restore one column's declared width
		-- Resetting the main column also restores its remaining-space behavior
		-- @param self table VirtualTree control
		-- @param column_name string stable column name
		-- @return boolean|nil changed
		-- @return string|nil error_text
		function control:reset_column_width(column_name)
			assert(type(column_name) == "string" and columns_by_name[column_name],
				"VirtualTree reset_column_width requires a known column name")
			local column = columns_by_name[column_name]
			column.width_kind = column.default_width_kind
			column.width = column.default_width
			if column == main_column then
				store_fit_to_width(true)
			end
			forget_column_geometry(self)

			return self:relayout()
		end

		--- Enable or suspend automatic main-column fitting
		-- Disabling freezes the currently displayed width so the column never jumps back to its declaration
		-- @param self table VirtualTree control
		-- @param enabled boolean requested state
		-- @return boolean|nil changed
		-- @return string|nil error_text
		function control:set_fit_to_width(enabled)
			assert(type(enabled) == "boolean", "VirtualTree set_fit_to_width requires a boolean")
			if fit_to_width == enabled then
				return true
			end
			if not enabled then
				main_column.width_kind = "dip"
				main_column.width = main_column.resolved_width or main_column.min_width
			end
			store_fit_to_width(enabled)
			forget_column_geometry(self)

			return self:relayout()
		end

		--- Select one provider sort without sorting or materializing application-owned rows
		-- The provider callback changes its own order before QUI drops only its bounded virtual cache
		-- @param self table VirtualTree control
		-- @param column_name string stable sortable column name
		-- @param direction string ascending or descending
		-- @return boolean|nil changed
		-- @return string|nil error_text
		function control:set_sort(column_name, direction)
			assert(type(column_name) == "string" and columns_by_name[column_name],
				"VirtualTree set_sort requires a known column name")
			local column = columns_by_name[column_name]
			assert(column.sortable, "VirtualTree set_sort requires a sortable column")
			assert(direction == "ascending" or direction == "descending",
				"VirtualTree set_sort direction must be 'ascending' or 'descending'")
			if sort_column == column and sort_direction == direction then
				return true
			end

			sort_column = column
			sort_direction = direction
			self.sort_column = column.name
			self.sort_direction = direction
			if on_sort then
				on_sort(self, column, direction)
			end

			return self:refresh()
		end

		--- Report the complete column width used by the Tree's automatic bottom Scrollbar
		-- @param self table VirtualTree control
		-- @param available integer visible row width
		-- @return integer content_width
		function control:_horizontal_content_width(available)
			local _, content_width = self:_arrange_columns(available)

			return content_width
		end

		--- Return one resizable divider under the pointer
		-- The generous 16-DIP zone follows Resonic's forgiving header interaction
		-- @param self table VirtualTree control
		-- @param x number pointer x
		-- @param row_right integer exclusive row edge
		-- @return table|nil column
		local function divider_at(self, x, row_right)
			local geometry = self:column_geometry(row_right)
			for index = #geometry, 1, -1 do
				local cell = geometry[index]
				local divider_x = cell.x - self.horizontal_offset + cell.width
				if cell.column.resizable and divider_x >= 1 and divider_x <= row_right + 8 and
					math.abs(x - divider_x) <= 8
				then
					return cell.column
				end
			end

			return nil
		end

		--- Return the column whose visible header cell contains one pointer position
		-- @param self table VirtualTree control
		-- @param x number pointer x
		-- @param row_right integer exclusive row edge
		-- @return table|nil column
		local function header_column_at(self, x, row_right)
			for _, cell in ipairs(self:column_geometry(row_right)) do
				local cell_x = cell.x - self.horizontal_offset
				if x >= cell_x and x < cell_x + cell.width then
					return cell.column
				end
			end

			return nil
		end

		--- Find the widest retained presentation without asking the virtual provider for hidden rows
		-- @param column table prepared column
		-- @return integer width
		local function known_column_width(column)
			local width = column.header_required_width or column.min_width
			for _, row in pairs(state.rows) do
				local variants = row.cell_presentations[column.name]
				if variants then
					for _, presentation in pairs(variants) do
						width = math.max(width, presentation.required_width or 0)
					end
				end
			end

			return width + 6
		end

		--- Fit one column to its known header and retained visible cell contents
		-- @param self table VirtualTree control
		-- @param column table prepared column
		local function fit_known_column(self, column)
			store_column_width(self, column, known_column_width(column))
		end

		--- Notify application code once after a completed user resize
		-- @param self table VirtualTree control
		-- @param column table prepared column
		-- @param changed boolean whether the visible width moved
		local function finish_column_resize(self, column, changed)
			if changed and on_column_resize then
				on_column_resize(self, column, column.width)
			end
		end

		--- Freeze every displayed width while one divider follows the pointer
		-- Fit-to-width is restored only after release so columns always resize toward the right
		-- @param self table VirtualTree control
		-- @param column table divider's column
		-- @param x number starting pointer x
		-- @param row_right integer exclusive row edge
		local function begin_column_resize(self, column, x, row_right)
			local geometry = self:column_geometry(row_right)
			for _, cell in ipairs(geometry) do
				cell.column.drag_width = cell.width
			end
			header_resize = {
				column = column,
				start_x = x,
				start_width = column.drag_width,
				start_fit = fit_to_width,
				changed = false,
			}
			store_fit_to_width(false)
			forget_column_geometry(self)
		end

		--- Remove temporary widths and apply the chosen width once
		-- @param self table VirtualTree control
		-- @param resize table completed interaction
		local function commit_column_resize(self, resize)
			local final_width = resize.column.drag_width
			for _, column in ipairs(columns) do
				column.drag_width = nil
			end
			resize.column.width_kind = "dip"
			resize.column.width = final_width
			if resize.column == main_column then
				store_fit_to_width(false)
			else
				store_fit_to_width(resize.start_fit)
			end
			forget_column_geometry(self)
		end

		--- Remove temporary widths without changing any declared column width
		-- @param self table VirtualTree control
		-- @param resize table cancelled interaction
		local function cancel_column_resize(self, resize)
			for _, column in ipairs(columns) do
				column.drag_width = nil
			end
			store_fit_to_width(resize.start_fit)
			forget_column_geometry(self)
		end

		--- Supply the pointer shape used after header handling falls through to ordinary rows
		function control:_pointer_cursor()
			return "arrow"
		end

		--- Handle Resonic-style divider sizing and fit-to-width gestures before row input
		-- @param event table normalized Control event
		-- @param row_right integer exclusive row edge
		-- @return table|nil result
		function control:_header_event(event, row_right)
			local in_header =
				type(event.y) == "number" and event.y >= 1 and event.y <= self._header_height
			if header_resize then
				if event.kind == "pointer_move" then
					header_resize.column.drag_width = math.max(
						header_resize.column.min_width,
						math.min(
							math.floor(header_resize.start_width + event.x - header_resize.start_x + 0.5),
							header_resize.column.max_width
						)
					)
					header_resize.changed =
						header_resize.column.drag_width ~= header_resize.start_width
					forget_column_geometry(self)
					return { handled = true, redraw = true, cursor = "size_we" }
				elseif event.kind == "pointer_up" and event.button == "left" then
					local resize = header_resize
					header_resize = nil
					if resize.changed then
						commit_column_resize(self, resize)
					else
						cancel_column_resize(self, resize)
					end
					finish_column_resize(self, resize.column, resize.changed)
					return { handled = true, redraw = true, capture = "release", cursor = "arrow" }
				elseif (event.kind == "key_down" and event.key == "escape") or
					event.kind == "focus_lost"
				then
					local resize = header_resize
					header_resize = nil
					cancel_column_resize(self, resize)
					if event.kind == "focus_lost" then
						self.focused = false
					end
					return { handled = true, redraw = true, capture = "release", cursor = "arrow" }
				end
			end

			if header_press then
				if event.kind == "pointer_move" or event.kind == "pointer_enter" then
					header_press.inside = in_header and
						header_column_at(self, event.x, row_right) == header_press.column
					return { handled = true, cursor = "arrow" }
				elseif event.kind == "pointer_leave" then
					header_press.inside = false
					return { handled = true, cursor = "arrow" }
				elseif event.kind == "pointer_up" and event.button == "left" then
					local press = header_press
					header_press = nil
					if press.inside then
						local direction = sort_column == press.column and
							sort_direction == "ascending" and "descending" or "ascending"
						local changed, sort_error = self:set_sort(press.column.name, direction)
						assert(changed, sort_error)
					end
					return { handled = true, redraw = press.inside, capture = "release", cursor = "arrow" }
				elseif (event.kind == "key_down" and event.key == "escape") or
					event.kind == "focus_lost"
				then
					header_press = nil
					if event.kind == "focus_lost" then
						self.focused = false
					end
					return { handled = true, redraw = true, capture = "release", cursor = "arrow" }
				end
			end

			if event.kind == "pointer_down" and event.button == "middle" and in_header then
				store_fit_to_width(true)
				forget_column_geometry(self)
				return { handled = true, redraw = true, focus = "acquire", cursor = "arrow" }
			end
			if not in_header then
				return nil
			end

			local column = divider_at(self, event.x, row_right)
			if event.kind == "pointer_enter" or event.kind == "pointer_move" then
				return {
					handled = true,
					cursor = column and "size_we" or "arrow",
				}
			elseif event.kind == "pointer_down" and event.button == "left" then
				if not column then
					local pressed_column = header_column_at(self, event.x, row_right)
					if pressed_column and pressed_column.sortable then
						header_press = { column = pressed_column, inside = true }
						return {
							handled = true,
							focus = "acquire",
							capture = "acquire",
							cursor = "arrow",
						}
					end
					return { handled = true, focus = "acquire", cursor = "arrow" }
				end
				local start_width = column.resolved_width or column.width
				if event.clicks >= 2 then
					fit_known_column(self, column)
					finish_column_resize(self, column, column.width ~= start_width)
					return { handled = true, redraw = true, focus = "acquire", cursor = "size_we" }
				end
				begin_column_resize(self, column, event.x, row_right)
				return {
					handled = true,
					focus = "acquire",
					capture = "acquire",
					cursor = "size_we",
				}
			elseif event.kind == "pointer_up" then
				return { handled = true, cursor = column and "size_we" or "arrow" }
			end

			return nil
		end
	end
	control = Tree(control)

	state.control = control
	control._virtual_rows = state
	control.visible_nodes = setmetatable({}, {
		__len = function()
			return container_rows(state.root)
		end,
		__index = function(_, index)
			return state:row(index)
		end,
	})
	control.visible_index = setmetatable({}, {
		__index = function(_, row)
			return row and row.row_index or nil
		end,
	})
	control.selected_node = initial_selection
	control.selected = initial_selection
	control._selected_node = nil
	control.on_change = on_change and function(self, _, node)
		on_change(self, node)
	end or nil

	if columns then
		--- Return a cached cell string, asking the provider only on first use
		-- @param row table materialized viewport row
		-- @param column table prepared column
		-- @return string caption
		function state:cell(row, column)
			local cached = row.cells[column.name]
			if cached ~= nil then
				return cached
			end

			cached = read_cell(row.source, column)
			row.cells[column.name] = cached

			return cached
		end

		--- Resolve column rectangles once for the current row width
		-- @param row_right integer exclusive row edge
		-- @return table geometry
		function control:column_geometry(row_right)
			return self:_arrange_columns(math.max(0, row_right - 1))
		end

		--- Return only columns intersecting the current viewport
		-- The small list is reused for every row until the width or horizontal position changes
		-- @param row_right integer exclusive row edge
		-- @return table geometry
		function control:visible_column_geometry(row_right)
			if self._visible_column_right ~= row_right or
				self._visible_column_offset ~= self.horizontal_offset
			then
				local visible = {}
				for _, cell in ipairs(self:column_geometry(row_right)) do
					local cell_x = cell.x - self.horizontal_offset
					-- A border-only sliver cannot show content and must not wake the virtual provider
					if cell_x + cell.width > 1 and cell_x + self.padding_x < row_right then
						visible[#visible + 1] = cell
					end
				end
				self._visible_column_geometry = visible
				self._visible_column_right = row_right
				self._visible_column_offset = self.horizontal_offset
			end

			return self._visible_column_geometry
		end

		--- Return the leftmost visible column's branch-arrow position for drawing and hit testing
		-- @param row table materialized viewport row
		-- @param row_right integer exclusive row edge
		-- @return integer x
		function control:_indicator_x(row, row_right)
			for _, cell in ipairs(self:column_geometry(row_right)) do
				if cell.column == tree_column then
					return cell.x - self.horizontal_offset + self.padding_x + row.depth * self.indent
				end
			end

			return self.padding_x + row.depth * self.indent
		end

		--- Draw the fixed header from the same geometry used by every row
		-- @param control_canvas userdata active drawing surface
		-- @param row_right integer exclusive row edge
		function control:_draw_header(control_canvas, row_right)
			local flags = { grayed = not self.enabled, down = false, hovering = false, focused = false }
			local text_color = self:get_color("text", flags)
			local line_color = self:get_color("frame", flags)
			local _, text_height = control_canvas:measure_text(self.font, "Mg")
			local text_y = 1 + math.floor((self._header_height - text_height) / 2)
			-- The ten-DIP slot produces Resonic's clear nine-by-five-pixel triangle at normal scale
			local sort_glyph_size = 10
			local sort_glyph_slot = sort_glyph_size + 5
			for _, cell in ipairs(self:visible_column_geometry(row_right)) do
				local cell_x = cell.x - self.horizontal_offset
				local shows_sort = cell.column == sort_column
				local reserved_sort_width = shows_sort and sort_glyph_slot or 0
				local caption, text_width, natural_width = fit_caption(
					control_canvas,
					self.font,
					cell.column.caption,
					cell.width - self.padding_x * 2 - reserved_sort_width
				)
				cell.column.header_required_width =
					natural_width + self.padding_x * 2 + reserved_sort_width
				local text_x = cell_x + self.padding_x
				if cell.column.align == "right" then
					-- A right-aligned caption keeps its right edge while the marker uses the opposite side
					text_x = cell_x + cell.width - self.padding_x - text_width
				elseif cell.column.align == "center" then
					text_x =
						cell_x + math.floor((cell.width - reserved_sort_width - text_width) / 2)
				end
				control_canvas:draw_text(self.font, caption, text_x, text_y, text_color)
				if shows_sort and cell.width >= self.padding_x * 2 + sort_glyph_slot then
					local glyph_x = cell_x + cell.width - self.padding_x - sort_glyph_size
					if cell.column.align == "right" then
						glyph_x = cell_x + self.padding_x
					end
					control_canvas:draw_glyph(
						sort_direction == "ascending" and
							glyphs.sort_ascending or glyphs.sort_descending,
						glyph_x,
						1 + math.floor((self._header_height - sort_glyph_size) / 2),
						sort_glyph_size,
						sort_glyph_size,
						self:get_color("symbol", flags)
					)
				end
				control_canvas:fill_rect(cell_x + cell.width - 1, 1, 1, self._header_height, line_color)
			end
			control_canvas:fill_rect(1, self._header_height, math.max(0, row_right - 1), 1, line_color)
		end

		--- Draw one visible row as independently cached cells
		-- @param control_canvas userdata active drawing surface
		-- @param row table materialized viewport row
		-- @param row_y integer row top
		-- @param row_right integer exclusive row edge
		-- @param flags table current row state
		function control:_draw_row(control_canvas, row, row_y, row_right, flags)
			local row_hovering = flags.hovering
			for _, cell in ipairs(self:visible_column_geometry(row_right)) do
				local cell_x = cell.x - self.horizontal_offset
				local caption = state:cell(row, cell.column)
				local context = row.cell_contexts[cell.column.name]
				if context == nil then
					-- Context and rectangle tables follow the bounded row cache instead of allocating every frame
					context = {
						rect = {},
						content_rect = {},
						text_rect = {},
						row_rect = {},
						style = {},
					}
					row.cell_contexts[cell.column.name] = context
				end

				context.node = row.source
				context.column = cell.column
				context.text = caption
				context.row_index = row.row_index
				context.depth = row.depth
				context.selected = row.source == self.selected_node
				context.hovering = row_hovering
				context.focused = self.focused and context.selected
				context.enabled = self.enabled and row.enabled
				context.rect.x = cell_x
				context.rect.y = row_y
				context.rect.width = cell.width
				context.rect.height = self.row_height
				context.row_rect.x = 1
				context.row_rect.y = row_y
				context.row_rect.width = math.max(0, row_right - 1)
				context.row_rect.height = self.row_height

				local variants = row.cell_presentations[cell.column.name]
				if variants == nil then
					variants = {}
					row.cell_presentations[cell.column.name] = variants
				end
				local state_key = cell_state_key(context)
				local presentation = variants[state_key]
				local fresh = presentation == nil
					or presentation.theme_generation ~= theme_generation
					or presentation.cell_x ~= cell.x
					or presentation.horizontal_offset ~= self.horizontal_offset
					or presentation.cell_width ~= cell.width
					or presentation.row_width ~= context.row_rect.width
					or presentation.row_height ~= self.row_height
					or presentation.control_width ~= self.arranged_width
					or presentation.control_height ~= self.arranged_height
					or presentation.base_font ~= self.font
				if fresh then
					local style = {
						font = self.font,
						text_color = self:get_color("text", flags),
						background = nil,
						align = cell.column.align,
					}
					context.style = style
					if cell_style then
						cell_style(self, context, style)
					end
					assert(style.font ~= nil, "VirtualTree cell_style must leave style.font available")
					assert(
						style.align == "left" or style.align == "center" or style.align == "right",
						"VirtualTree cell_style align must be 'left', 'center', or 'right'"
					)
					style.text_color = theme.resolve(style.text_color)
					if style.background ~= nil then
						style.background = theme.resolve(style.background)
					end

					local background_commands = {}
					if draw_cell_background then
						background_commands = record_drawing(
							control_canvas,
							context.rect.x,
							context.rect.y,
							draw_cell_background,
							self,
							context,
							style
						)
					end
					presentation = {
						theme_generation = theme_generation,
						cell_x = cell.x,
						horizontal_offset = self.horizontal_offset,
						cell_width = cell.width,
						row_width = context.row_rect.width,
						row_height = self.row_height,
						control_width = self.arranged_width,
						control_height = self.arranged_height,
						base_font = self.font,
						style = style,
						background_commands = background_commands,
					}
					variants[state_key] = presentation
				else
					context.style = presentation.style
				end

				local style = presentation.style
				if style.background ~= nil then
					-- Ordinary hover and selection stay readable unless custom drawing deliberately paints over them
					if not context.hovering and not context.selected then
						control_canvas:fill_rect(
							context.rect.x,
							context.rect.y,
							context.rect.width,
							context.rect.height,
							style.background
						)
					end
				end
				replay_drawing(control_canvas, presentation.background_commands, context.rect.x, context.rect.y)

				local text_x = cell_x + self.padding_x
				local available = cell.width - self.padding_x * 2
				if cell.column == tree_column then
					local indicator_x = cell_x + self.padding_x + row.depth * self.indent
					if row.has_children then
						local indicator_y = row_y + math.floor((self.row_height - self.indicator_size) / 2)
						flags.hovering = row == self.hovered_indicator_node
						control_canvas:draw_glyph(
							row.expanded and glyphs.tree_expanded or glyphs.tree_collapsed,
							indicator_x,
							indicator_y,
							self.indicator_size,
							self.indicator_size,
							self:get_color("symbol", flags)
						)
					end
					text_x = indicator_x + self.indent
					available = cell_x + cell.width - self.padding_x - text_x
				end
				available = math.max(0, available)
				local content_x = text_x
				local text_width
				local natural_width
				caption, text_width, natural_width =
					fit_caption(control_canvas, style.font, caption, available)
				presentation.required_width = natural_width + self.padding_x * 2
				if cell.column == tree_column then
					presentation.required_width =
						presentation.required_width + row.depth * self.indent + self.indent
				end
				if style.align == "right" then
					text_x = content_x + available - text_width
				elseif style.align == "center" then
					text_x = content_x + math.floor((available - text_width) / 2)
				end
				local _, text_height = control_canvas:measure_text(style.font, "Mg")
				local text_y = row_y + math.floor((self.row_height - text_height) / 2)
				context.content_rect.x = content_x
				context.content_rect.y = row_y
				context.content_rect.width = math.max(0, available)
				context.content_rect.height = self.row_height
				context.text_rect.x = text_x
				context.text_rect.y = text_y
				context.text_rect.width = text_width
				context.text_rect.height = text_height
				flags.hovering = row_hovering
				control_canvas:draw_text(style.font, caption, text_x, text_y, style.text_color)
				if fresh then
					local overlay_commands = {}
					if draw_cell_overlay then
						overlay_commands = record_drawing(
							control_canvas,
							context.rect.x,
							context.rect.y,
							draw_cell_overlay,
							self,
							context,
							style
						)
					end
					presentation.overlay_commands = overlay_commands
				end
				replay_drawing(control_canvas, presentation.overlay_commands, context.rect.x, context.rect.y)
			end
		end
	end

	--- Select one node that is currently visible, selected, or retained as an expanded branch
	-- @param source any|nil application-owned node identity
	-- @return boolean|nil changed
	-- @return string|nil error_text
	function control:set_selected(source)
		if source == nil then
			local previous = self._selected_node
			self._selected_node = nil
			self.selected_node = nil
			self.selected = nil
			local invalidated, invalidate_error = self:redraw()
			if not invalidated then
				self._selected_node = previous
				self.selected_node = previous and previous.source or nil
				self.selected = self.selected_node

				return nil, invalidate_error
			end
			if previous and self.on_change then
				self:on_change(nil, nil)
			end

			return true
		end
		if self.selected_node == source then
			return true
		end

		local row = state.source_rows[source]
		local branch = state.branches_by_source[source]
		row = row or branch and branch.row or nil
		if not row then
			return nil, "VirtualTree node is not currently materialized"
		end
		if not row.enabled then
			return nil, "VirtualTree selection must name an enabled node"
		end

		local previous = self._selected_node
		self._selected_node = row
		self.selected_node = source
		self.selected = source
		local previous_first = self.first_visible
		state:reveal(self, row.row_index)
		local invalidated, invalidate_error = self:redraw()
		if not invalidated then
			self._selected_node = previous
			self.selected_node = previous and previous.source or nil
			self.selected = self.selected_node
			self.first_visible = previous_first

			return nil, invalidate_error
		end
		if self.on_change then
			self:on_change(self.selected, self.selected_node)
		end

		return true
	end

	--- Expand or collapse one currently materialized application-owned node
	-- @param source any application-owned node identity
	-- @param expanded boolean requested state
	-- @return boolean|nil changed
	-- @return string|nil error_text
	function control:set_expanded(source, expanded)
		assert(type(expanded) == "boolean", "VirtualTree expanded state must be a boolean")
		local row = state.source_rows[source]
		local branch = state.branches_by_source[source]
		row = row or branch and branch.row or nil
		if not row then
			return nil, "VirtualTree node is not currently materialized"
		end

		return state:set_expanded(self, row, expanded, true)
	end

	--- Refresh one materialized node, or reset all sparse state when node is nil
	-- @param source any|nil application-owned node identity
	-- @return boolean|nil refreshed
	-- @return string|nil error_text
	function control:refresh(source)
		if source == nil then
			state.root.count = checked_count(root_count(), "VirtualTree root_count")
			state.root.branches = {}
			state.root.branches_by_index = {}
			state.branches_by_source = {}
			self._selected_node = nil
			self.selected_node = nil
			self.selected = nil
		else
			local row = state.source_rows[source]
			local branch = state.branches_by_source[source]
			row = row or branch and branch.row or nil
			if not row then
				return nil, "VirtualTree node is not currently materialized"
			end

			local caption, enabled, branch_available = describe(source)
			row.text = caption
			row.enabled = enabled
			row.has_children = branch_available
			if branch and branch.expanded then
				if on_expand then
					on_expand(source)
				end
				branch.count = branch_available
					and checked_count(child_count(source), "VirtualTree child_count") or 0
				branch.expanded = branch.count > 0
			end
		end
		state:clear_rows()

		return self:relayout()
	end

	if columns then
		local invalidate_control = control.invalidate

		--- Force complete cell presentation rebuilding before the ordinary Window invalidation
		-- Focused refresh methods below clear only their requested presentation entries
		function control:invalidate()
			state:clear_presentations()

			return invalidate_control(self)
		end

		--- Forget one cached cell and redraw if that node is currently retained
		-- @param source any application-owned node identity
		-- @param column_name string stable column name
		-- @return boolean|nil invalidated
		-- @return string|nil error_text
		function control:invalidate_cell(source, column_name)
			assert(source ~= nil, "VirtualTree invalidate_cell requires a node")
			assert(type(column_name) == "string" and columns_by_name[column_name],
				"VirtualTree invalidate_cell requires a known column name")
			local row = state.source_rows[source]
			local branch = state.branches_by_source[source]
			row = row or branch and branch.row or nil
			if not row then
				return true
			end

			row.cells[column_name] = nil
			row.cell_presentations[column_name] = nil

			return self:redraw()
		end

		--- Forget every cached cell for one retained node
		-- @param source any application-owned node identity
		-- @return boolean|nil invalidated
		-- @return string|nil error_text
		function control:invalidate_node(source)
			assert(source ~= nil, "VirtualTree invalidate_node requires a node")
			local row = state.source_rows[source]
			local branch = state.branches_by_source[source]
			row = row or branch and branch.row or nil
			if not row then
				return true
			end

			row.cells = {}
			row.cell_presentations = {}

			return self:redraw()
		end

		--- Forget one cached column across all recently retained rows
		-- @param column_name string stable column name
		-- @return boolean|nil invalidated
		-- @return string|nil error_text
		function control:invalidate_column(column_name)
			assert(type(column_name) == "string" and columns_by_name[column_name],
				"VirtualTree invalidate_column requires a known column name")
			for _, row in pairs(state.source_rows) do
				row.cells[column_name] = nil
				row.cell_presentations[column_name] = nil
			end

			return self:redraw()
		end

		--- Forget every cell value in the current viewport
		-- @return boolean|nil invalidated
		-- @return string|nil error_text
		function control:invalidate_visible()
			local visible = self:_visible_row_count()
			local final_index = math.min(#self.visible_nodes, self.first_visible + visible - 1)
			for index = self.first_visible, final_index do
				local row = state:row(index)
				row.cells = {}
				row.cell_presentations = {}
			end

			return self:redraw()
		end
	end

	return control
end

-- Callable module syntax keeps declarations readable as 'VirtualTree { ... }'
setmetatable(VirtualTree, {
	__call = function(_, options)
		return VirtualTree.new(options)
	end,
})

return VirtualTree
