Skip to content
This repository was archived by the owner on Nov 7, 2026. It is now read-only.
Closed
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
16 changes: 15 additions & 1 deletion .github/workflows/core-tests.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: VisuStructCore tests
name: CI

on:
push:
Expand All @@ -17,3 +17,17 @@ jobs:
run: swift --version
- name: Build and test
run: swift test

app-build:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Xcode version
run: xcodebuild -version
- name: Build app for iOS Simulator
run: |
xcodebuild build \
-project VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct.xcodeproj \
-scheme VisuStruct \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO
23 changes: 22 additions & 1 deletion VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,10 @@ struct ContentView: View {
@ViewBuilder
private var simulationDetailView: some View {
GeometryReader { geo in
let useColumns = geo.size.width >= 760
// Seitenspalte nur im Querformat; hochkant wäre sie gequetscht und unter dem Diagramm bliebe Platz frei.
let useColumns = geo.size.width >= 760 && geo.size.width > geo.size.height
// iPad hochkant: Diagramm oben, Steuerfeld darunter über die volle Breite.
let useWideStack = !useColumns && geo.size.width >= 600 && geo.size.height >= 700
let panelWidth = min(max(geo.size.width * 0.32, 280), 360)

if useColumns {
Expand All @@ -792,6 +795,24 @@ struct ContentView: View {
.frame(width: panelWidth)
.frame(maxHeight: .infinity, alignment: .top)
}
} else if useWideStack {
VStack(alignment: .leading, spacing: 16) {
simulationCanvasBlock
.frame(height: geo.size.height * 0.5)
SimulationStatePanel(
engine: simulationEngine,
isPlaying: isSimulationPlaying,
inputText: $simulationInputText,
highlightModeRaw: $simulationHighlightModeRaw,
onPlay: playSimulation,
onPause: pauseSimulation,
onStep: stepSimulation,
onSubmitInput: submitSimulationInput,
onStop: resetSimulation,
usesWideLayout: true
)
.frame(maxHeight: .infinity)
}
} else {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct SimulationStatePanel: View {
let onStep: () -> Void
let onSubmitInput: () -> Void
let onStop: () -> Void
/// Hochkant auf dem iPad: Steuerung links, Schritt/Variablen/Ausgabe rechts statt alles untereinander.
var usesWideLayout: Bool = false

private var sortedVariables: [(String, VisuStructSimulationValue)] {
engine.state.variables
Expand All @@ -26,7 +28,9 @@ struct SimulationStatePanel: View {

var body: some View {
Group {
if isWaitingForInput {
if usesWideLayout {
wideStack
} else if isWaitingForInput {
ScrollView {
panelStack(flexibleCardsFillHeight: false)
}
Expand All @@ -50,125 +54,169 @@ struct SimulationStatePanel: View {
@ViewBuilder
private func panelStack(flexibleCardsFillHeight: Bool) -> some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 10) {
Label(lang.tr("simulation_controls"), systemImage: "slider.horizontal.3")
.font(.headline)
controlsSection
inputCard
traceCard(flexibleCardsFillHeight: flexibleCardsFillHeight)
variablesCard(flexibleCardsFillHeight: flexibleCardsFillHeight)
outputCard(flexibleCardsFillHeight: flexibleCardsFillHeight)
.layoutPriority(flexibleCardsFillHeight ? 1 : 0)
noticeText
}
}

LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) {
SimulationControlButton(title: lang.tr("simulation_play"), systemImage: "play.fill", isDisabled: isPlaying || !engine.canStep, action: onPlay)
SimulationControlButton(title: lang.tr("simulation_pause"), systemImage: "pause.fill", isDisabled: !isPlaying, action: onPause)
SimulationControlButton(title: lang.tr("simulation_step"), systemImage: "arrow.right.circle", isDisabled: isPlaying || !engine.canStep, action: onStep)
SimulationControlButton(title: lang.tr("simulation_stop"), systemImage: "stop.fill", isDisabled: false, action: onStop)
private var wideStack: some View {
HStack(alignment: .top, spacing: 16) {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
controlsSection
inputCard
noticeText
}
}
.scrollBounceBehavior(.basedOnSize)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)

VStack(alignment: .leading, spacing: 6) {
Text(lang.tr("simulation_highlight_label"))
.font(.caption)
.foregroundStyle(.secondary)
Picker(lang.tr("simulation_highlight_label"), selection: $highlightModeRaw) {
Text(lang.tr("simulation_highlight_last")).tag(SimulationHighlightMode.lastExecuted.rawValue)
Text(lang.tr("simulation_highlight_next")).tag(SimulationHighlightMode.nextToExecute.rawValue)
}
.pickerStyle(.segmented)
VStack(alignment: .leading, spacing: 16) {
traceCard(flexibleCardsFillHeight: false)
HStack(alignment: .top, spacing: 16) {
variablesCard(flexibleCardsFillHeight: true)
outputCard(flexibleCardsFillHeight: true)
}
.frame(maxHeight: .infinity)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}

SimulationInputCard(
prompt: engine.state.inputRequest?.prompt,
variableName: engine.state.inputRequest?.variableName,
valueType: engine.state.inputRequest?.valueType,
inputError: engine.state.inputErrorNotice?.localized(in: lang),
inputText: $inputText,
onSubmit: onSubmitInput
)
private var controlsSection: some View {
VStack(alignment: .leading, spacing: 10) {
Label(lang.tr("simulation_controls"), systemImage: "slider.horizontal.3")
.font(.headline)

SimulationPlaceholderCard(
title: lang.tr("simulation_trace"),
systemImage: "arrow.left.arrow.right",
minHeight: flexibleCardsFillHeight ? 130 : 88
) {
if let trace = engine.state.lastTrace {
Text(trace)
.font(.body.monospaced())
.frame(maxWidth: .infinity, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
} else {
Text(lang.tr("simulation_trace_placeholder"))
.font(.body)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) {
SimulationControlButton(title: lang.tr("simulation_play"), systemImage: "play.fill", isDisabled: isPlaying || !engine.canStep, action: onPlay)
SimulationControlButton(title: lang.tr("simulation_pause"), systemImage: "pause.fill", isDisabled: !isPlaying, action: onPause)
SimulationControlButton(title: lang.tr("simulation_step"), systemImage: "arrow.right.circle", isDisabled: isPlaying || !engine.canStep, action: onStep)
SimulationControlButton(title: lang.tr("simulation_stop"), systemImage: "stop.fill", isDisabled: false, action: onStop)
}

SimulationPlaceholderCard(
title: lang.tr("simulation_variables"),
systemImage: "curlybraces",
minHeight: flexibleCardsFillHeight ? 190 : 96,
contentFillsHeight: flexibleCardsFillHeight
) {
if sortedVariables.isEmpty {
Text(lang.tr("simulation_variables_placeholder"))
.font(.body)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
} else {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
ForEach(sortedVariables, id: \.0) { name, value in
HStack(alignment: .firstTextBaseline) {
Text(name)
.font(.body.monospaced().weight(.semibold))
Spacer(minLength: 12)
Text(value.description)
.font(.body.monospaced())
.foregroundStyle(.secondary)
}
}
}
.padding(.trailing, 14)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
VStack(alignment: .leading, spacing: 6) {
Text(lang.tr("simulation_highlight_label"))
.font(.caption)
.foregroundStyle(.secondary)
Picker(lang.tr("simulation_highlight_label"), selection: $highlightModeRaw) {
Text(lang.tr("simulation_highlight_last")).tag(SimulationHighlightMode.lastExecuted.rawValue)
Text(lang.tr("simulation_highlight_next")).tag(SimulationHighlightMode.nextToExecute.rawValue)
}
.pickerStyle(.segmented)
}
}
}

SimulationPlaceholderCard(
title: lang.tr("simulation_output"),
systemImage: "terminal",
minHeight: flexibleCardsFillHeight ? 170 : 96,
contentFillsHeight: flexibleCardsFillHeight
) {
if engine.state.outputLines.isEmpty {
Text(lang.tr("simulation_output_placeholder"))
.font(.body)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
} else {
ScrollView {
VStack(alignment: .leading, spacing: 6) {
ForEach(Array(engine.state.outputLines.enumerated()), id: \.offset) { _, line in
Text(line)
private var inputCard: some View {
SimulationInputCard(
prompt: engine.state.inputRequest?.prompt,
variableName: engine.state.inputRequest?.variableName,
valueType: engine.state.inputRequest?.valueType,
inputError: engine.state.inputErrorNotice?.localized(in: lang),
inputText: $inputText,
onSubmit: onSubmitInput
)
}

private func traceCard(flexibleCardsFillHeight: Bool) -> some View {
SimulationPlaceholderCard(
title: lang.tr("simulation_trace"),
systemImage: "arrow.left.arrow.right",
minHeight: flexibleCardsFillHeight ? 130 : 88
) {
if let trace = engine.state.lastTrace {
Text(trace)
.font(.body.monospaced())
.frame(maxWidth: .infinity, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
} else {
Text(lang.tr("simulation_trace_placeholder"))
.font(.body)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}

private func variablesCard(flexibleCardsFillHeight: Bool) -> some View {
SimulationPlaceholderCard(
title: lang.tr("simulation_variables"),
systemImage: "curlybraces",
minHeight: flexibleCardsFillHeight ? 190 : 96,
contentFillsHeight: flexibleCardsFillHeight
) {
if sortedVariables.isEmpty {
Text(lang.tr("simulation_variables_placeholder"))
.font(.body)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
} else {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
ForEach(sortedVariables, id: \.0) { name, value in
HStack(alignment: .firstTextBaseline) {
Text(name)
.font(.body.monospaced().weight(.semibold))
Spacer(minLength: 12)
Text(value.description)
.font(.body.monospaced())
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(.secondary)
}
}
.padding(.trailing, 4)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(.trailing, 14)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
.layoutPriority(flexibleCardsFillHeight ? 1 : 0)
}
}

if let message = engine.state.notice?.localized(in: lang) {
Text(message)
.font(.caption)
private func outputCard(flexibleCardsFillHeight: Bool) -> some View {
SimulationPlaceholderCard(
title: lang.tr("simulation_output"),
systemImage: "terminal",
minHeight: flexibleCardsFillHeight ? 170 : 96,
contentFillsHeight: flexibleCardsFillHeight
) {
if engine.state.outputLines.isEmpty {
Text(lang.tr("simulation_output_placeholder"))
.font(.body)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
} else {
ScrollView {
VStack(alignment: .leading, spacing: 6) {
ForEach(Array(engine.state.outputLines.enumerated()), id: \.offset) { _, line in
Text(line)
.font(.body.monospaced())
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(.trailing, 4)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
}

@ViewBuilder
private var noticeText: some View {
if let message = engine.state.notice?.localized(in: lang) {
Text(message)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}

}

private struct SimulationInputCard: View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
"simulation_input_submit" = "OK";
"simulation_input_invalid_int" = "يرجى إدخال عدد صحيح.";
"simulation_trace" = "آخر خطوة";
"simulation_trace_placeholder" = "سيتم شرح آخر خطوة تم تنفيذها هنا.";
"simulation_trace_placeholder" = "لم يُنفَّذ أي خطوة بعد.";
"simulation_variables" = "المتغيرات";
"simulation_variables_placeholder" = "ستظهر قيم المتغيرات الحالية هنا لاحقًا.";
"simulation_variables_placeholder" = "لا توجد متغيرات بعد.";
"simulation_output" = "المخرجات";
"simulation_output_placeholder" = "ستظهر مخرجات المحاكاة هنا لاحقًا.";
"simulation_output_placeholder" = "لا يوجد إخراج بعد.";
"action_delete_selection" = "حذف التحديد";
"ctx_copy" = "نسخ";
"ctx_cut" = "قص";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
"simulation_input_submit" = "OK";
"simulation_input_invalid_int" = "Indtast et heltal.";
"simulation_trace" = "Sidste trin";
"simulation_trace_placeholder" = "Det senest udførte trin forklares her.";
"simulation_trace_placeholder" = "Intet trin udført endnu.";
"simulation_variables" = "Variabler";
"simulation_variables_placeholder" = "Aktuelle variabelværdier vises her senere.";
"simulation_variables_placeholder" = "Ingen variabler endnu.";
"simulation_output" = "Output";
"simulation_output_placeholder" = "Simulationens output vises her senere.";
"simulation_output_placeholder" = "Intet output endnu.";
"action_delete_selection" = "Slet markering";
"ctx_copy" = "Kopiér";
"ctx_cut" = "Klip";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
"simulation_input_submit" = "OK";
"simulation_input_invalid_int" = "Bitte eine ganze Zahl eingeben.";
"simulation_trace" = "Letzter Schritt";
"simulation_trace_placeholder" = "Hier wird später der zuletzt ausgeführte Schritt erklärt.";
"simulation_trace_placeholder" = "Noch kein Schritt ausgeführt.";
"simulation_variables" = "Variablen";
"simulation_variables_placeholder" = "Hier werden später die aktuellen Variablenwerte angezeigt.";
"simulation_variables_placeholder" = "Noch keine Variablen.";
"simulation_output" = "Ausgabe";
"simulation_output_placeholder" = "Hier erscheint später die Ausgabe der Simulation.";
"simulation_output_placeholder" = "Noch keine Ausgabe.";
"action_delete_selection" = "Auswahl löschen";
"ctx_copy" = "Kopieren";
"ctx_cut" = "Ausschneiden";
Expand Down
Loading
Loading