Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
env:
STUDIO_PIECES: ${{ github.workspace }}/build/pieces
run: |
& $env:GODOT --headless --path . --script res://tests/run.gd -- contract studio pieces connect recolour shared_pieces mcp
& $env:GODOT --headless --path . --script res://tests/run.gd -- contract studio pieces connect recolour shared_pieces mcp ui
if ($LASTEXITCODE -ne 0) { throw 'A suite failed.' }
- name: Downloads and window cancellation with Windows curl
timeout-minutes: 3
Expand Down Expand Up @@ -267,7 +267,7 @@ jobs:
env:
STUDIO_PIECES: ${{ github.workspace }}/build/pieces
run: |
& $env:GODOT --headless --path . --script res://tests/run.gd -- contract studio pieces connect recolour shared_pieces mcp
& $env:GODOT --headless --path . --script res://tests/run.gd -- contract studio pieces connect recolour shared_pieces mcp ui
if ($LASTEXITCODE -ne 0) { throw 'A suite failed.' }
- name: Downloads and window cancellation with Godot's HTTP client
timeout-minutes: 3
Expand Down
73 changes: 62 additions & 11 deletions src/app/main.gd
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ var _section: Texture2D
var _orbiting := false
var _closing := false
var _window_pids := []
var _empty_preview: CenterContainer
var _preview_hint: PanelContainer
## What closing the window ends with; tests name another.
var on_quit := func(): get_tree().quit()

Expand Down Expand Up @@ -143,6 +145,7 @@ func _open() -> void:
root.set_anchors_preset(Control.PRESET_FULL_RECT)
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
layer.add_child(root)
_preview_guidance()
home = HomePanel.new()
root.add_child(home)
panel = StudioPanel.new()
Expand Down Expand Up @@ -200,13 +203,54 @@ func _layout() -> void:
var view := root.size
if view.x <= 0.0:
return
var width := minf(PANEL_WIDTH, view.x * 0.45) if view.x >= 1000.0 else minf(PANEL_WIDTH * 1.4, view.x - GAP * 2.0)
var width := minf(PANEL_WIDTH, view.x - GAP * 2.0)
for side: Control in [home, panel]:
side.set_anchors_preset(Control.PRESET_TOP_LEFT)
side.offset_left = GAP
side.offset_top = GAP
side.offset_right = GAP + width
side.offset_bottom = maxf(view.y - GAP, GAP)
_empty_preview.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_empty_preview.offset_left = width + GAP * 2.0
_empty_preview.offset_right = -GAP
_empty_preview.offset_top = GAP
_empty_preview.offset_bottom = -GAP
_preview_hint.size = _preview_hint.get_combined_minimum_size()
_preview_hint.position = Vector2((width + GAP + view.x - _preview_hint.size.x) / 2.0, GAP)
var dialog := layer.get_node_or_null("ConnectAgentDialog") as AcceptDialog
if dialog:
UiSkin.refit_dialog.call_deferred(dialog)

func _preview_guidance() -> void:
_empty_preview = CenterContainer.new()
_empty_preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
root.add_child(_empty_preview)
var card := PanelContainer.new()
card.theme_type_variation = "Inset"
card.mouse_filter = Control.MOUSE_FILTER_IGNORE
card.custom_minimum_size.x = 320
_empty_preview.add_child(card)
var text := VBoxContainer.new()
text.add_theme_constant_override("separation", 8)
text.mouse_filter = Control.MOUSE_FILTER_IGNORE
card.add_child(text)
for entry in [["Your preview appears here", "Heading"], ["Bring a model or recolor a look\nto see it on the stage.", "Muted"]]:
var line := Label.new()
line.text = entry[0]
line.theme_type_variation = entry[1]
line.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
line.mouse_filter = Control.MOUSE_FILTER_IGNORE
text.add_child(line)
_preview_hint = PanelContainer.new()
_preview_hint.theme_type_variation = "Pill"
_preview_hint.mouse_filter = Control.MOUSE_FILTER_IGNORE
_preview_hint.hide()
root.add_child(_preview_hint)
var hint := Label.new()
hint.text = "Drag the preview to rotate"
hint.theme_type_variation = "Muted"
hint.mouse_filter = Control.MOUSE_FILTER_IGNORE
_preview_hint.add_child(hint)

func _show_home() -> void:
if home == null or flow != null:
Expand Down Expand Up @@ -323,6 +367,9 @@ func open_window() -> bool:

func _process(_delta: float) -> void:
_window_pids = _window_pids.filter(func(pid): return OS.get_process_exit_code(pid) == -1)
if stage != null:
_empty_preview.visible = stage.models.is_empty()
_preview_hint.visible = not stage.models.is_empty()

static func window_command(exe: String, args: Array, windows := OS.get_name() == "Windows") -> Array:
if windows:
Expand Down Expand Up @@ -367,29 +414,31 @@ func _connect_dialog(exe := OS.get_executable_path()) -> void:
ask.ok_button_text = "Done"
ask.theme = skin.theme
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 6)
box.custom_minimum_size.x = CONNECT_WIDTH
box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
box.add_theme_constant_override("separation", 12)
ask.add_child(box)
var title := Label.new()
title.text = "Connect an agent"
title.theme_type_variation = "Heading"
box.add_child(title)
var intro := Label.new()
intro.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
intro.text = "Run one of these once in %s. Afterwards your agent can make characters and companions with Studio, with no window. If you move this app to another folder, run the command again." % ("Terminal" if OS.get_name() == "macOS" else "PowerShell")
intro.custom_minimum_size = Vector2(CONNECT_WIDTH, 0)
box.add_child(intro)
var scroll := ScrollContainer.new()
scroll.name = "Commands"
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
scroll.custom_minimum_size = Vector2(CONNECT_WIDTH + 12, minf(420.0, get_viewport().get_visible_rect().size.y - 220.0))
box.add_child(scroll)
var list := VBoxContainer.new()
list.name = "Commands"
list.add_theme_constant_override("separation", 6)
list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.add_child(list)
box.add_child(list)
var before := [] if OS.has_feature("template") else ["--path", ProjectSettings.globalize_path("res://").trim_suffix("/")]
for entry in connect_commands(exe, before):
var heading := Label.new()
heading.text = entry[0]
heading.theme_type_variation = "Section"
list.add_child(heading)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
list.add_child(row)
var text := TextEdit.new()
text.text = entry[1]
Expand All @@ -400,6 +449,8 @@ func _connect_dialog(exe := OS.get_executable_path()) -> void:
row.add_child(text)
var copy := Button.new()
copy.text = "Copy"
copy.custom_minimum_size.x = 80
copy.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
copy.tooltip_text = "Copy the %s command" % entry[0]
var command: String = entry[1]
copy.pressed.connect(func():
Expand All @@ -410,9 +461,9 @@ func _connect_dialog(exe := OS.get_executable_path()) -> void:
docker.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
docker.theme_type_variation = "Muted"
docker.text = "Docker Agent: add the toolset to your agent in its YAML file, with root replaced by your agent's name."
docker.custom_minimum_size = Vector2(CONNECT_WIDTH, 0)
box.add_child(docker)
layer.add_child(ask)
UiSkin.frame_dialog(ask, title)
ask.confirmed.connect(ask.queue_free)
ask.canceled.connect(ask.queue_free)
ask.popup_centered()
UiSkin.popup_dialog(ask, int(CONNECT_WIDTH + ask.get_theme_stylebox("panel").get_minimum_size().x))
38 changes: 29 additions & 9 deletions src/ui/home_panel.gd
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func _build() -> void:
_note("Make characters and companions for Agent Guild from a model you have, or by recoloring a world's own.")
var label := Label.new()
label.text = "World"
label.theme_type_variation = "Field"
rows.add_child(label)
var pick := OptionButton.new()
for id in WORLD_ORDER:
Expand Down Expand Up @@ -95,20 +96,39 @@ func _build() -> void:
if pieces.is_empty():
_note("Pieces you save are listed here, to edit later.")
for piece: Dictionary in pieces:
var line := HBoxContainer.new()
rows.add_child(line)
var card := PanelContainer.new()
card.theme_type_variation = "Inset"
rows.add_child(card)
var entry := VBoxContainer.new()
entry.add_theme_constant_override("separation", 6)
card.add_child(entry)
var name := Label.new()
name.text = "%s · %s" % [piece.title, piece.slot]
name.size_flags_horizontal = Control.SIZE_EXPAND_FILL
name.clip_text = true
name.tooltip_text = String(piece.get("saved", ""))
line.add_child(name)
name.text = piece.title
name.theme_type_variation = "Strong"
name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
name.tooltip_text = "%s\n%s" % [piece.title, piece.get("saved", "")]
entry.add_child(name)
var slot := Label.new()
slot.text = piece.slot
slot.theme_type_variation = "Muted"
slot.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
slot.tooltip_text = piece.slot
entry.add_child(slot)
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 8)
entry.add_child(line)
var id: String = piece.id
var edit := _button("Edit", func(): edit_requested.emit(id), line)
edit.disabled = art == "downloading" or piece.error != ""
_button("Remove", func(): remove_requested.emit(id), line)
var remove := _button("Remove", func(): remove_requested.emit(id), line)
remove.theme_type_variation = "Quiet"
remove.tooltip_text = "Remove from this list. The saved folder is kept."
if piece.error != "":
_note("It can't be edited: %s." % piece.error, "Note")
var problem := Label.new()
problem.text = "It can't be edited: %s." % piece.error
problem.theme_type_variation = "Note"
problem.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
entry.add_child(problem)
_heading("Agents", false)
_note("An agent can make pieces with Studio for you.")
_button("Connect an agent…", func(): connect_requested.emit())
Expand Down
20 changes: 17 additions & 3 deletions src/ui/studio_panel.gd
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ func _colour_controls(rebuild: Callable) -> void:
else:
var choose := Label.new()
choose.text = "Color to change"
choose.theme_type_variation = "Field"
rows.add_child(choose)
_swatches = HFlowContainer.new()
_swatches.add_theme_constant_override("h_separation", 8)
Expand All @@ -352,6 +353,7 @@ func _colour_controls(rebuild: Callable) -> void:
var row := HBoxContainer.new()
var into := Label.new()
into.text = "Change it to"
into.theme_type_variation = "Strong"
into.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(into)
_to = _picker(tints.to if tints.to is Color else Color.WHITE, func(c):
Expand Down Expand Up @@ -559,6 +561,7 @@ func _show_preview(kind: String, notes: Array) -> void:
func _adjust(kind: String) -> void:
var label := Label.new()
label.text = "Adjust"
label.theme_type_variation = "Field"
rows.add_child(label)
if kind == "character":
_slider("Height", 0.85, 1.15, float(options.get("height_share", 1.0)), 0.01, func(v): _set_option("height_share", v),
Expand All @@ -585,6 +588,7 @@ func _show_finish() -> void:
_editing_line()
var label := Label.new()
label.text = "Title"
label.theme_type_variation = "Field"
rows.add_child(label)
var title := LineEdit.new()
title.text = editing.title if not editing.is_empty() else (String(files[0]).get_file().get_basename() if recolour.is_empty() and not files.is_empty() else "%s recolored" % where.name).capitalize()
Expand Down Expand Up @@ -717,12 +721,15 @@ func _action(text: String, busy: String, pressed: Callable, into: Container = nu
return b

func _choice(label_text: String, items: Array, current: String, picked: Callable) -> void:
var row := HBoxContainer.new()
var row := VBoxContainer.new()
row.add_theme_constant_override("separation", 6)
var label := Label.new()
label.text = label_text
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.theme_type_variation = "Field"
row.add_child(label)
var pick := OptionButton.new()
pick.size_flags_horizontal = Control.SIZE_EXPAND_FILL
pick.clip_text = true
for item in items:
pick.add_item(item[0])
pick.select(maxi(0, items.map(func(i): return i[1]).find(current)))
Expand Down Expand Up @@ -757,20 +764,27 @@ func _slider(label_text: String, low: float, high: float, value: float, step_by:
var row := HBoxContainer.new()
var label := Label.new()
label.text = label_text
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.theme_type_variation = "Strong"
row.add_child(label)
var amount := Label.new()
amount.theme_type_variation = "Muted"
amount.size_flags_horizontal = Control.SIZE_EXPAND_FILL
amount.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
amount.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
amount.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
amount.text = say.call(value)
amount.tooltip_text = amount.text
row.add_child(amount)
var slider := HSlider.new()
slider.min_value = low
slider.max_value = high
slider.step = step_by
slider.value = value
slider.custom_minimum_size.y = 28
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
slider.value_changed.connect(func(v):
amount.text = say.call(v)
amount.tooltip_text = amount.text
changed.call(v))
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 2)
Expand Down
25 changes: 23 additions & 2 deletions src/ui/ui_skin.gd
Original file line number Diff line number Diff line change
Expand Up @@ -494,8 +494,11 @@ func _buttons() -> void:

# Choice: one of a set, filled in the accent while chosen.
t.set_type_variation("Choice", "Button")
t.set_stylebox("pressed", "Choice", primary_down)
t.set_stylebox("hover_pressed", "Choice", primary)
var choices := {"normal": normal, "hover": hover, "pressed": primary_down, "hover_pressed": primary, "disabled": disabled}
for state in choices:
var choice: StyleBoxFlat = choices[state].duplicate()
_margins(choice, 10, 8, 10, 8)
t.set_stylebox(state, "Choice", choice)
for c in ["font_pressed_color", "font_hover_pressed_color"]:
t.set_color(c, "Choice", color("accent-text"))

Expand Down Expand Up @@ -582,6 +585,7 @@ func _fields() -> void:
t.set_stylebox("focus", type, focused)
t.set_stylebox("read_only", type, field)
t.set_color("font_color", type, color("text"))
t.set_color("font_readonly_color" if type == "TextEdit" else "font_uneditable_color", type, color("text"))
t.set_color("font_placeholder_color", type, color("muted", 0.85))
t.set_color("caret_color", type, color("accent"))
t.set_color("selection_color", type, color("accent", 0.3))
Expand All @@ -590,6 +594,23 @@ func _fields() -> void:
lane.border_color = color("border")
t.set_stylebox("background", "ProgressBar", lane)
t.set_stylebox("fill", "ProgressBar", _flat(color("accent"), r))
var track := _flat(color("border"), 2)
track.content_margin_top = 2
track.content_margin_bottom = 2
t.set_stylebox("slider", "HSlider", track)
var filled := track.duplicate()
filled.bg_color = color("accent")
t.set_stylebox("grabber_area", "HSlider", filled)
var active := filled.duplicate()
active.bg_color = color("focus")
t.set_stylebox("grabber_area_highlight", "HSlider", active)
var knobs := {"grabber": "accent", "grabber_highlight": "focus", "grabber_disabled": "muted"}
for state in knobs:
var c := color(knobs[state])
t.set_icon(state, "HSlider", DPITexture.create_from_string("<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18'><circle cx='9' cy='9' r='7' fill='#%s' stroke='#%s' stroke-width='2'/></svg>" % [c.to_html(false), color("surface").to_html(false)]))
var slider_focus := focused.duplicate()
slider_focus.draw_center = false
t.set_stylebox("focus", "HSlider", slider_focus)
t.set_color("font_color", "LinkButton", color("accent"))
t.set_color("font_hover_color", "LinkButton", color("gold-ink"))

Expand Down
Loading
Loading