use super::*;
impl GodotBridge {
pub(crate) fn mountUi<A: Clone + 'static>(
&self,
root: &Container<A>,
theme: &AppTheme,
) -> GodoruResult<GodotUiTree<A>> {
let mut bridge = self.clone();
bridge.methods = GodotMethods::load(&bridge)?;
bridge.mountUiReady(root, theme)
}
pub(crate) fn mountUiReady<A: Clone + 'static>(
&self,
root: &Container<A>,
theme: &AppTheme,
) -> GodoruResult<GodotUiTree<A>> {
let engine = self.singleton("Engine")?;
let mainLoop = self.callObject(self.methods.engineGetMainLoop, engine, &[])?;
if mainLoop.0.is_null() {
return Err(GodoruError::GodotRuntime(
"Engine.get_main_loop returned null".to_string(),
));
}
let window = self.callObject(self.methods.sceneTreeGetRoot, mainLoop, &[])?;
if window.0.is_null() {
return Err(GodoruError::GodotRuntime(
"SceneTree.get_root returned null".to_string(),
));
}
let godoruRoot = self.construct("Control")?;
self.setName(godoruRoot, "GodoruRoot")?;
self.fullRect(godoruRoot)?;
self.addChild(window, godoruRoot)?;
let background = self.construct("ColorRect")?;
self.setName(background, "GodoruBackground")?;
self.colorRectSetColor(background, &theme.backgroundColor)?;
self.fullRect(background)?;
self.addChild(godoruRoot, background)?;
let mut resources = Vec::new();
let mut interactiveNodes = Vec::new();
self.mountContainer(
root,
theme,
godoruRoot,
0,
true,
&mut resources,
&mut interactiveNodes,
)?;
Ok(GodotUiTree {
bridge: self.clone(),
root: godoruRoot,
resources,
interactiveNodes,
})
}
fn mountContainer<A: Clone + 'static>(
&self,
container: &Container<A>,
theme: &AppTheme,
parent: GodotObject,
index: usize,
isRoot: bool,
resources: &mut Vec<GodotObject>,
interactiveNodes: &mut Vec<InteractiveNode<A>>,
) -> GodoruResult<GodotObject> {
let style = resolveStyle(
theme,
Component::Container,
container.class.as_deref(),
&container.style,
);
let margin = container
.margin
.clone()
.or_else(|| style.margin.clone())
.unwrap_or_default();
let padding = container
.padding
.clone()
.or_else(|| style.padding.clone())
.unwrap_or_default();
let panel = self.construct(if hasVisualStyle(&style) {
"PanelContainer"
} else {
"Control"
})?;
self.setName(
panel,
&nodeName(container.id.as_deref(), "Container", index),
)?;
if isRoot {
self.fullRect(panel)?;
self.setOffsets(panel, &margin)?;
} else {
self.applySize(panel, container)?;
}
if let Some(cursor) = container.cursor.as_ref() {
self.setCursor(panel, cursor)?;
}
self.addChild(parent, panel)?;
if container.interactions.hasLocalInput() {
self.enableLocalInput(panel)?;
interactiveNodes.push(InteractiveNode {
object: panel,
component: Component::Container,
class: container.class.clone(),
localStyle: container.style.clone(),
interactions: container.interactions.clone(),
textValue: None,
checkedValue: None,
onChange: None,
onSubmit: None,
onToggle: None,
pressed: false,
hovered: false,
focused: false,
});
}
if hasVisualStyle(&style) {
self.applyControlStyle(panel, &style, "panel", resources)?;
} else if let Some(background) = style.background.as_ref() {
let backgroundNode = self.construct("ColorRect")?;
self.setName(backgroundNode, "Background")?;
self.colorRectSetColor(backgroundNode, background)?;
self.fullRect(backgroundNode)?;
self.addChild(panel, backgroundNode)?;
}
if style.shader.is_some() {
let shaderSurface = self.construct("ColorRect")?;
self.setName(shaderSurface, "ShaderSurface")?;
self.colorRectSetColor(
shaderSurface,
style
.background
.as_ref()
.unwrap_or(&Color::rgb(1.0, 1.0, 1.0)),
)?;
self.fullRect(shaderSurface)?;
self.applyShader(shaderSurface, style.shader.as_ref(), resources)?;
self.addChild(panel, shaderSurface)?;
}
let layout = self.construct(layoutClass(container))?;
self.setName(layout, "Layout")?;
self.fullRect(layout)?;
self.setOffsets(layout, &padding)?;
self.addThemeConstantOverride(layout, "separation", container.gap.unwrap_or(0) as i64)?;
self.addChild(panel, layout)?;
for (childIndex, child) in container.children.iter().enumerate() {
match child {
UiNode::Container(child) => {
self.mountContainer(
child,
theme,
layout,
childIndex,
false,
resources,
interactiveNodes,
)?;
}
UiNode::Text(text) => {
self.mountText(text, theme, layout, childIndex, resources, interactiveNodes)?;
}
UiNode::Image(image) => {
self.mountImage(
image,
theme,
layout,
childIndex,
resources,
interactiveNodes,
)?;
}
UiNode::Button(button) => {
self.mountButton(
button,
theme,
layout,
childIndex,
resources,
interactiveNodes,
)?;
}
UiNode::TextInput(textInput) => {
self.mountTextInput(
textInput,
theme,
layout,
childIndex,
resources,
interactiveNodes,
)?;
}
UiNode::Checkbox(checkbox) => {
self.mountCheckbox(
checkbox,
theme,
layout,
childIndex,
resources,
interactiveNodes,
)?;
}
UiNode::Scroll(scroll) => {
self.mountScroll(
scroll,
theme,
layout,
childIndex,
resources,
interactiveNodes,
)?;
}
}
}
Ok(panel)
}
fn mountText<A: Clone + 'static>(
&self,
text: &Text<A>,
theme: &AppTheme,
parent: GodotObject,
index: usize,
resources: &mut Vec<GodotObject>,
interactiveNodes: &mut Vec<InteractiveNode<A>>,
) -> GodoruResult<GodotObject> {
let style = resolveStyle(theme, Component::Text, text.class.as_deref(), &text.style);
let label = self.construct("Label")?;
self.setName(label, &nodeName(text.id.as_deref(), "Text", index))?;
self.labelSetText(label, &text.value)?;
if let Some(cursor) = text.cursor.as_ref() {
self.setCursor(label, cursor)?;
}
if let Some(color) = style.color.as_ref() {
self.addThemeColorOverride(label, "font_color", color)?;
}
if let Some(size) = style.fontSize {
self.addThemeFontSizeOverride(label, "font_size", size as i64)?;
}
self.applyTextStyle(label, &style, resources)?;
self.applyShader(label, style.shader.as_ref(), resources)?;
self.addChild(parent, label)?;
if text.interactions.hasLocalInput() {
self.enableLocalInput(label)?;
interactiveNodes.push(InteractiveNode {
object: label,
component: Component::Text,
class: text.class.clone(),
localStyle: text.style.clone(),
interactions: text.interactions.clone(),
textValue: None,
checkedValue: None,
onChange: None,
onSubmit: None,
onToggle: None,
pressed: false,
hovered: false,
focused: false,
});
}
Ok(label)
}
fn mountImage<A: Clone + 'static>(
&self,
image: &Image<A>,
theme: &AppTheme,
parent: GodotObject,
index: usize,
resources: &mut Vec<GodotObject>,
interactiveNodes: &mut Vec<InteractiveNode<A>>,
) -> GodoruResult<GodotObject> {
let style = resolveStyle(
theme,
Component::Image,
image.class.as_deref(),
&image.style,
);
let texture = self.loadImageTextureVariant(&image.image.runtimePath(), resources)?;
let textureRect = self.construct("TextureRect")?;
self.setName(textureRect, &nodeName(image.id.as_deref(), "Image", index))?;
self.setProperty(textureRect, "texture", &texture)?;
self.setTextureRectFit(textureRect, &image.fit)?;
if let Some(cursor) = image.cursor.as_ref() {
self.setCursor(textureRect, cursor)?;
}
if let Some((width, height)) = image.customMinimumSize {
self.setCustomMinimumSize(textureRect, width, height)?;
}
self.applyControlStyle(textureRect, &style, "panel", resources)?;
self.applyShader(textureRect, style.shader.as_ref(), resources)?;
self.addChild(parent, textureRect)?;
if image.interactions.hasLocalInput() {
self.enableLocalInput(textureRect)?;
interactiveNodes.push(InteractiveNode {
object: textureRect,
component: Component::Image,
class: image.class.clone(),
localStyle: image.style.clone(),
interactions: image.interactions.clone(),
textValue: None,
checkedValue: None,
onChange: None,
onSubmit: None,
onToggle: None,
pressed: false,
hovered: false,
focused: false,
});
}
Ok(textureRect)
}
fn mountButton<A: Clone + 'static>(
&self,
button: &Button<A>,
theme: &AppTheme,
parent: GodotObject,
index: usize,
resources: &mut Vec<GodotObject>,
interactiveNodes: &mut Vec<InteractiveNode<A>>,
) -> GodoruResult<GodotObject> {
let style = resolveStyle(
theme,
Component::Button,
button.class.as_deref(),
&button.style,
);
let node = self.construct("Button")?;
self.setName(node, &nodeName(button.id.as_deref(), "Button", index))?;
self.buttonSetText(node, &button.text)?;
self.setCursor(node, button.cursor.as_ref().unwrap_or(&CursorIcon::Pointer))?;
if let Some((width, height)) = button.customMinimumSize {
self.setCustomMinimumSize(node, width, height)?;
}
self.applyTextStyle(node, &style, resources)?;
self.applyButtonStyle(
node,
theme,
button.class.as_deref(),
&button.style,
resources,
)?;
self.applyShader(node, style.shader.as_ref(), resources)?;
self.addChild(parent, node)?;
if button.interactions.hasLocalInput() {
self.enableLocalInput(node)?;
interactiveNodes.push(InteractiveNode {
object: node,
component: Component::Button,
class: button.class.clone(),
localStyle: button.style.clone(),
interactions: button.interactions.clone(),
textValue: None,
checkedValue: None,
onChange: None,
onSubmit: None,
onToggle: None,
pressed: false,
hovered: false,
focused: false,
});
}
Ok(node)
}
fn mountTextInput<A: Clone + 'static>(
&self,
textInput: &TextInput<A>,
theme: &AppTheme,
parent: GodotObject,
index: usize,
resources: &mut Vec<GodotObject>,
interactiveNodes: &mut Vec<InteractiveNode<A>>,
) -> GodoruResult<GodotObject> {
let style = resolveStyle(
theme,
Component::TextInput,
textInput.class.as_deref(),
&textInput.style,
);
let node = self.construct("LineEdit")?;
self.setName(node, &nodeName(textInput.id.as_deref(), "TextInput", index))?;
self.lineEditSetText(node, &textInput.value)?;
self.setCursor(node, textInput.cursor.as_ref().unwrap_or(&CursorIcon::Text))?;
if let Some(placeholder) = textInput.placeholder.as_ref() {
self.lineEditSetPlaceholder(node, placeholder)?;
}
if let Some((width, height)) = textInput.customMinimumSize {
self.setCustomMinimumSize(node, width, height)?;
}
self.applyTextStyle(node, &style, resources)?;
self.applyControlStyle(node, &style, "normal", resources)?;
self.applyShader(node, style.shader.as_ref(), resources)?;
self.enableLocalInput(node)?;
self.addChild(parent, node)?;
interactiveNodes.push(InteractiveNode {
object: node,
component: Component::TextInput,
class: textInput.class.clone(),
localStyle: textInput.style.clone(),
interactions: textInput.interactions.clone(),
textValue: Some(textInput.value.clone()),
checkedValue: None,
onChange: textInput.onChange.clone(),
onSubmit: textInput.onSubmit.clone(),
onToggle: None,
pressed: false,
hovered: false,
focused: false,
});
Ok(node)
}
fn mountCheckbox<A: Clone + 'static>(
&self,
checkbox: &Checkbox<A>,
theme: &AppTheme,
parent: GodotObject,
index: usize,
resources: &mut Vec<GodotObject>,
interactiveNodes: &mut Vec<InteractiveNode<A>>,
) -> GodoruResult<GodotObject> {
let style = resolveStyle(
theme,
Component::Checkbox,
checkbox.class.as_deref(),
&checkbox.style,
);
let node = self.construct("CheckBox")?;
self.setName(node, &nodeName(checkbox.id.as_deref(), "Checkbox", index))?;
if let Some(text) = checkbox.text.as_ref() {
self.buttonSetText(node, text)?;
}
self.setCursor(
node,
checkbox.cursor.as_ref().unwrap_or(&CursorIcon::Pointer),
)?;
self.setProperty(node, "button_pressed", &self.variantBool(checkbox.checked))?;
if let Some((width, height)) = checkbox.customMinimumSize {
self.setCustomMinimumSize(node, width, height)?;
}
self.applyTextStyle(node, &style, resources)?;
self.applyButtonStyle(
node,
theme,
checkbox.class.as_deref(),
&checkbox.style,
resources,
)?;
self.applyShader(node, style.shader.as_ref(), resources)?;
self.enableLocalInput(node)?;
self.addChild(parent, node)?;
interactiveNodes.push(InteractiveNode {
object: node,
component: Component::Checkbox,
class: checkbox.class.clone(),
localStyle: checkbox.style.clone(),
interactions: checkbox.interactions.clone(),
textValue: None,
checkedValue: Some(checkbox.checked),
onChange: None,
onSubmit: None,
onToggle: checkbox.onToggle.clone(),
pressed: checkbox.checked,
hovered: false,
focused: false,
});
Ok(node)
}
fn mountScroll<A: Clone + 'static>(
&self,
scroll: &Scroll<A>,
theme: &AppTheme,
parent: GodotObject,
index: usize,
resources: &mut Vec<GodotObject>,
interactiveNodes: &mut Vec<InteractiveNode<A>>,
) -> GodoruResult<GodotObject> {
let style = resolveStyle(
theme,
Component::Scroll,
scroll.class.as_deref(),
&scroll.style,
);
let frame = if hasVisualStyle(&style) || style.shader.is_some() {
let frame = self.construct("PanelContainer")?;
self.setName(frame, &nodeName(scroll.id.as_deref(), "Scroll", index))?;
if let Some(cursor) = scroll.cursor.as_ref() {
self.setCursor(frame, cursor)?;
}
if let Some((width, height)) = scroll.customMinimumSize {
self.setCustomMinimumSize(frame, width, height)?;
}
self.applyControlStyle(frame, &style, "panel", resources)?;
self.applyShader(frame, style.shader.as_ref(), resources)?;
self.addChild(parent, frame)?;
Some(frame)
} else {
None
};
let node = self.construct("ScrollContainer")?;
let nodeName = if frame.is_some() {
"ScrollViewport".to_string()
} else {
nodeName(scroll.id.as_deref(), "Scroll", index)
};
self.setName(node, &nodeName)?;
if frame.is_none()
&& let Some(cursor) = scroll.cursor.as_ref()
{
self.setCursor(node, cursor)?;
}
if frame.is_none()
&& let Some((width, height)) = scroll.customMinimumSize
{
self.setCustomMinimumSize(node, width, height)?;
}
self.scrollMode(node)?;
if frame.is_some() {
self.fullRect(node)?;
}
self.addChild(frame.unwrap_or(parent), node)?;
let content = self.construct("VBoxContainer")?;
self.setName(content, "Content")?;
self.fullRect(content)?;
self.addChild(node, content)?;
for (childIndex, child) in scroll.children.iter().enumerate() {
match child {
UiNode::Container(child) => {
self.mountContainer(
child,
theme,
content,
childIndex,
false,
resources,
interactiveNodes,
)?;
}
UiNode::Text(text) => {
self.mountText(
text,
theme,
content,
childIndex,
resources,
interactiveNodes,
)?;
}
UiNode::Image(image) => {
self.mountImage(
image,
theme,
content,
childIndex,
resources,
interactiveNodes,
)?;
}
UiNode::Button(button) => {
self.mountButton(
button,
theme,
content,
childIndex,
resources,
interactiveNodes,
)?;
}
UiNode::TextInput(textInput) => {
self.mountTextInput(
textInput,
theme,
content,
childIndex,
resources,
interactiveNodes,
)?;
}
UiNode::Checkbox(checkbox) => {
self.mountCheckbox(
checkbox,
theme,
content,
childIndex,
resources,
interactiveNodes,
)?;
}
UiNode::Scroll(child) => {
self.mountScroll(
child,
theme,
content,
childIndex,
resources,
interactiveNodes,
)?;
}
}
}
Ok(frame.unwrap_or(node))
}
}
pub(crate) fn resolveStyle(
theme: &AppTheme,
component: Component,
class: Option<&str>,
local: &Style,
) -> Style {
let mut style = theme.styleFor(component.clone(), class);
crate::ui::mergeStyle(&mut style, local);
validateStyle(component, &style);
style
}
pub(crate) fn validateStyle(component: Component, style: &Style) {
#[cfg(debug_assertions)]
{
let visual = style.background.is_some()
|| style.radius.is_some()
|| style.border.is_some()
|| style.shadow.is_some();
if component == Component::Text && visual {
eprintln!("Godoru style warning: Text ignores background, radius, border and shadow");
}
if !matches!(
component,
Component::Text | Component::Button | Component::TextInput | Component::Checkbox
) && (style.font.is_some() || style.fontSize.is_some() || style.fontWeight.is_some())
{
eprintln!("Godoru style warning: non-text component ignores font styles");
}
}
let _ = (component, style);
}
pub(crate) fn hasVisualStyle(style: &Style) -> bool {
style.background.is_some()
|| style.radius.is_some()
|| style.border.is_some()
|| style.shadow.is_some()
}
pub(crate) fn layoutClass<A>(container: &Container<A>) -> &'static str {
match (&container.direction, &container.wrap) {
(ContainerDirection::Vertical, ContainerWrap::NoWrap) => "VBoxContainer",
(ContainerDirection::Horizontal, ContainerWrap::NoWrap) => "HBoxContainer",
(ContainerDirection::Vertical, ContainerWrap::Wrap)
| (ContainerDirection::Vertical, ContainerWrap::WrapReverse) => "VFlowContainer",
(ContainerDirection::Horizontal, ContainerWrap::Wrap)
| (ContainerDirection::Horizontal, ContainerWrap::WrapReverse) => "HFlowContainer",
}
}
pub(crate) fn sizeFlags(mode: &SizeMode) -> i64 {
match mode {
SizeMode::Fill | SizeMode::Percent(_) => SIZE_FILL_EXPAND,
SizeMode::Fit | SizeMode::Fixed(_) => 0,
}
}
pub(crate) fn nodeName(id: Option<&str>, fallback: &str, index: usize) -> String {
let mut name = String::new();
if let Some(id) = id {
for character in id.chars() {
if character.is_ascii_alphanumeric() || character == '_' {
name.push(character);
}
}
}
if name.is_empty() {
format!("{fallback}{index}")
} else {
name
}
}