using System;
using System.Linq;
using System.Threading;
using AIImages.Helpers;
using AIImages.Models;
using AIImages.Services;
using RimWorld;
using UnityEngine;
using Verse;
#pragma warning disable IDE1006 // Naming Styles
namespace AIImages
{
///
/// Окно для просмотра персонажа и генерации AI изображений
///
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Style",
"IDE1006:Naming Styles",
Justification = "RimWorld Window naming convention"
)]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Minor Code Smell",
"S101:Types should be named in PascalCase",
Justification = "RimWorld Window naming convention"
)]
public class Window_AIImage : Window
{
private Pawn pawn;
private PawnAppearanceData appearanceData;
private StableDiffusionSettings generationSettings;
private Texture2D generatedImage;
private bool isGenerating = false;
private string generationStatus = "";
private CancellationTokenSource cancellationTokenSource;
// Прогресс генерации
private double generationProgress = 0.0;
private int currentStep = 0;
private int totalSteps = 0;
private double etaSeconds = 0.0;
// Сервисы (получаем через DI)
private readonly IPawnDescriptionService pawnDescriptionService;
private readonly IPromptGeneratorService promptGeneratorService;
private readonly IStableDiffusionApiService apiService;
public Window_AIImage(Pawn pawn)
{
this.pawn = pawn;
this.doCloseX = true;
this.doCloseButton = false; // Убираем дублирующую кнопку "Закрыть"
this.forcePause = false;
this.absorbInputAroundWindow = false;
this.draggable = true;
this.preventCameraMotion = false;
// Получаем сервисы через DI контейнер
var services = AIImagesMod.Services;
pawnDescriptionService = services.PawnDescriptionService;
promptGeneratorService = services.PromptGeneratorService;
apiService = services.ApiService;
// Извлекаем данные персонажа
RefreshPawnData();
}
public override Vector2 InitialSize => new Vector2(900f, 800f);
private Vector2 mainScrollPosition = Vector2.zero;
private Vector2 promptScrollPosition = Vector2.zero;
private Vector2 negativePromptScrollPosition = Vector2.zero;
private float copiedMessageTime = 0f;
// Состояние сворачиваемых секций промптов
private bool showPositivePrompt = false;
private bool showNegativePrompt = false;
///
/// Обновляет данные персонажа
///
private void RefreshPawnData()
{
DebugLogger.Log($"[AI Images] RefreshPawnData called for {pawn?.Name}");
appearanceData = pawnDescriptionService.ExtractAppearanceData(pawn);
generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings();
// Загружаем сохраненный портрет, если есть
LoadSavedPortrait();
DebugLogger.Log($"[AI Images] RefreshPawnData completed for {pawn?.Name}");
}
///
/// Загружает сохраненный портрет персонажа
///
private void LoadSavedPortrait()
{
DebugLogger.Log($"[AI Images] LoadSavedPortrait called for {pawn?.Name}");
if (PawnPortraitHelper.HasPortrait(pawn))
{
DebugLogger.Log($"[AI Images] Portrait found for {pawn?.Name}, loading...");
generatedImage = PawnPortraitHelper.LoadPortrait(pawn);
if (generatedImage != null)
{
generationStatus = "AIImages.Generation.LoadedFromSave".Translate();
DebugLogger.Log($"[AI Images] Successfully loaded portrait for {pawn?.Name}");
DebugLogger.Log(
$"[AI Images] generatedImage is now set: {generatedImage != null}, size: {generatedImage.width}x{generatedImage.height}"
);
}
else
{
DebugLogger.Warning(
$"[AI Images] Failed to load portrait texture for {pawn?.Name}"
);
}
}
else
{
DebugLogger.Log($"[AI Images] No saved portrait found for {pawn?.Name}");
}
}
///
/// Освобождает ресурсы при закрытии окна
///
public override void PreClose()
{
base.PreClose();
// Отменяем генерацию, если она идет
if (isGenerating && cancellationTokenSource != null)
{
cancellationTokenSource.Cancel();
}
// Освобождаем CancellationTokenSource
cancellationTokenSource?.Dispose();
}
///
/// Обновляет текущую пешку в окне
///
public void UpdatePawn(Pawn newPawn)
{
DebugLogger.Log(
$"[AI Images] UpdatePawn called - switching from {pawn?.Name} to {newPawn?.Name}"
);
this.pawn = newPawn;
// Очищаем старое изображение при смене персонажа
generatedImage = null;
generationStatus = "";
generationProgress = 0.0;
currentStep = 0;
totalSteps = 0;
etaSeconds = 0.0;
// Сбрасываем состояние сворачиваемых секций
showPositivePrompt = false;
showNegativePrompt = false;
// Обновляем данные персонажа (включая загрузку портрета)
RefreshPawnData();
// Принудительно обновляем окно
this.windowRect = new Rect(
this.windowRect.x,
this.windowRect.y,
this.InitialSize.x,
this.InitialSize.y
);
DebugLogger.Log($"[AI Images] UpdatePawn completed for {newPawn?.Name}");
}
///
/// Получить текущую пешку
///
public Pawn CurrentPawn => pawn;
///
/// Отладочный метод для проверки состояния всех пешек
///
public static void DebugAllPawns()
{
DebugLogger.Log("[AI Images] === DEBUG: Checking all pawns ===");
if (Current.Game?.Maps == null)
{
DebugLogger.Log("[AI Images] No game or maps available");
return;
}
int totalPawns = 0;
int pawnsWithComponents = 0;
int pawnsWithPortraits = 0;
foreach (var map in Current.Game.Maps)
{
foreach (var pawn in map.mapPawns.AllPawns)
{
if (pawn.RaceProps?.Humanlike == true)
{
totalPawns++;
var (hasComponent, hasPortrait) = CheckPawnPortraitStatus(pawn);
if (hasComponent)
pawnsWithComponents++;
if (hasPortrait)
pawnsWithPortraits++;
}
}
}
LogDebugSummary(totalPawns, pawnsWithComponents, pawnsWithPortraits);
}
private static (bool hasComponent, bool hasPortrait) CheckPawnPortraitStatus(Pawn pawn)
{
var comp = PawnPortraitHelper.GetPortraitComp(pawn);
if (comp != null)
{
if (comp.HasPortrait)
{
DebugLogger.Log(
$"[AI Images] {pawn.Name}: Has component with portrait '{comp.PortraitPath}'"
);
return (true, true);
}
else
{
DebugLogger.Log($"[AI Images] {pawn.Name}: Has component but no portrait");
return (true, false);
}
}
else
{
DebugLogger.Warning($"[AI Images] {pawn.Name}: No portrait component found!");
return (false, false);
}
}
private static void LogDebugSummary(
int totalPawns,
int pawnsWithComponents,
int pawnsWithPortraits
)
{
DebugLogger.Log($"[AI Images] === DEBUG SUMMARY ===");
DebugLogger.Log($"[AI Images] Total humanlike pawns: {totalPawns}");
DebugLogger.Log($"[AI Images] Pawns with components: {pawnsWithComponents}");
DebugLogger.Log($"[AI Images] Pawns with portraits: {pawnsWithPortraits}");
DebugLogger.Log($"[AI Images] === END DEBUG ===");
}
///
/// Вызывается каждый кадр для обновления окна
///
public override void WindowUpdate()
{
base.WindowUpdate();
// Проверяем, изменилась ли выбранная пешка
Pawn selectedPawn = Find.Selector.SelectedPawns.FirstOrDefault(p =>
p.IsColonist && p.Spawned && p.Faction == Faction.OfPlayer
);
// Если выбрана новая колонистская пешка, обновляем окно
if (selectedPawn != null && selectedPawn != pawn)
{
UpdatePawn(selectedPawn);
}
// Уменьшаем таймер сообщения о копировании
if (copiedMessageTime > 0f)
{
copiedMessageTime -= Time.deltaTime;
}
}
///
/// Асинхронная генерация изображения с поддержкой отмены
///
private async System.Threading.Tasks.Task GenerateImageAsync()
{
if (isGenerating)
return;
// Создаем новый CancellationTokenSource
cancellationTokenSource?.Dispose();
cancellationTokenSource = new CancellationTokenSource();
isGenerating = true;
generationStatus = "AIImages.Generation.InProgress".Translate();
generationProgress = 0.0;
currentStep = 0;
totalSteps = generationSettings.Steps;
try
{
// Генерируем промпты
string positivePrompt = promptGeneratorService.GeneratePositivePrompt(
appearanceData,
generationSettings
);
string negativePrompt = promptGeneratorService.GenerateNegativePrompt(
generationSettings
);
// Создаем запрос
var request = new GenerationRequest
{
Prompt = positivePrompt,
NegativePrompt = negativePrompt,
Steps = generationSettings.Steps,
CfgScale = generationSettings.CfgScale,
Width = generationSettings.Width,
Height = generationSettings.Height,
Sampler = generationSettings.Sampler,
Scheduler = generationSettings.Scheduler,
Seed = generationSettings.Seed,
Model = AIImagesMod.Settings.apiEndpoint,
SaveImagesToServer = AIImagesMod.Settings.saveImagesToServer,
};
// Создаем отдельный CancellationTokenSource для мониторинга прогресса
var progressCts = new CancellationTokenSource();
var progressTask = MonitorProgressAsync(progressCts.Token);
// Генерируем изображение с поддержкой отмены
var result = await apiService.GenerateImageAsync(
request,
cancellationTokenSource.Token
);
// Останавливаем мониторинг прогресса
progressCts.Cancel();
try
{
await progressTask;
}
catch (OperationCanceledException)
{
// Ожидаемое исключение при остановке мониторинга
}
finally
{
progressCts.Dispose();
}
if (result.Success)
{
DebugLogger.Log($"[AI Images] Generation successful for {pawn?.Name}");
DebugLogger.Log(
$"[AI Images] Image data size: {result.ImageData?.Length ?? 0} bytes"
);
DebugLogger.Log($"[AI Images] Saved path: {result.SavedPath}");
// Загружаем текстуру
generatedImage = new Texture2D(2, 2);
generatedImage.LoadImage(result.ImageData);
generationStatus = "AIImages.Generation.Success".Translate();
generationProgress = 1.0;
// Сохраняем путь к портрету на персонаже
DebugLogger.Log(
$"[AI Images] About to save portrait path for {pawn?.Name}: {result.SavedPath}"
);
PawnPortraitHelper.SavePortraitPath(pawn, result.SavedPath);
Messages.Message(
"AIImages.Generation.SavedTo".Translate(result.SavedPath),
MessageTypeDefOf.PositiveEvent
);
DebugLogger.Log(
$"[AI Images] Portrait generation and saving completed for {pawn?.Name}"
);
}
else
{
generationStatus =
$"AIImages.Generation.Failed".Translate() + ": {result.ErrorMessage}";
Messages.Message(generationStatus, MessageTypeDefOf.RejectInput);
}
}
catch (OperationCanceledException)
{
generationStatus = "AIImages.Generation.Cancelled".Translate();
DebugLogger.Log("[AI Images] Generation cancelled by user");
}
catch (Exception ex)
{
generationStatus = $"Error: {ex.Message}";
DebugLogger.Error($"[AI Images] Generation error: {ex}");
Messages.Message(
$"AIImages.Generation.Error".Translate() + ": {ex.Message}",
MessageTypeDefOf.RejectInput
);
}
finally
{
isGenerating = false;
}
}
///
/// Мониторит прогресс генерации и обновляет UI
///
private async System.Threading.Tasks.Task MonitorProgressAsync(
CancellationToken cancellationToken
)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
var progress = await apiService.GetProgressAsync(cancellationToken);
if (progress != null && progress.IsActive)
{
generationProgress = progress.Progress;
currentStep = progress.CurrentStep;
totalSteps = progress.TotalSteps;
etaSeconds = progress.EtaRelative;
DebugLogger.Log(
$"[AI Images] Progress: {progress.Progress:P} - Step {progress.CurrentStep}/{progress.TotalSteps} - ETA: {progress.EtaRelative:F1}s"
);
}
// Обновляем каждые 500ms
await System.Threading.Tasks.Task.Delay(500, cancellationToken);
}
}
catch (OperationCanceledException)
{
// Ожидаемое исключение при остановке
}
catch (Exception ex)
{
DebugLogger.Warning($"[AI Images] Progress monitoring error: {ex.Message}");
}
}
///
/// Запускает генерацию изображения (обертка для безопасного fire-and-forget)
///
private void StartGeneration()
{
AsyncHelper.FireAndForget(GenerateImageAsync(), "Image Generation");
}
///
/// Отменяет генерацию изображения
///
private void CancelGeneration()
{
if (isGenerating && cancellationTokenSource != null)
{
cancellationTokenSource.Cancel();
generationStatus = "AIImages.Generation.Cancelling".Translate();
}
}
public override void DoWindowContents(Rect inRect)
{
// Рассчитываем общую высоту контента
float totalContentHeight = CalculateTotalContentHeight();
Rect viewRect = new Rect(0f, 0f, inRect.width - 20f, totalContentHeight);
Widgets.BeginScrollView(inRect, ref mainScrollPosition, viewRect);
float curY = 0f;
// Заголовок
Text.Font = GameFont.Medium;
Widgets.Label(
new Rect(0f, curY, viewRect.width, 40f),
"AIImages.Window.Title".Translate()
);
curY += 45f;
// Имя пешки
Text.Font = GameFont.Small;
Widgets.Label(
new Rect(0f, curY, viewRect.width, 30f),
"AIImages.Window.PawnLabel".Translate(pawn.NameShortColored.Resolve())
);
curY += 40f;
// Разделитель
Widgets.DrawLineHorizontal(0f, curY, viewRect.width);
curY += 10f;
// Разделяем на две колонки: левая - информация, правая - изображение
float leftColumnWidth = viewRect.width * 0.35f;
float rightColumnWidth = viewRect.width * 0.62f;
float columnGap = viewRect.width * 0.03f;
// Определяем высоту колонок (берем большую из двух)
float columnHeight = Mathf.Max(CalculateContentHeight(), CalculateRightColumnHeight());
// Левая колонка - информация
Rect leftColumnRect = new Rect(0f, curY, leftColumnWidth, columnHeight);
DrawLeftColumn(leftColumnRect);
// Правая колонка - превью и управление
Rect rightColumnRect = new Rect(
leftColumnWidth + columnGap,
curY,
rightColumnWidth,
columnHeight
);
DrawRightColumn(rightColumnRect);
Widgets.EndScrollView();
}
private void DrawLeftColumn(Rect rect)
{
float contentY = rect.y;
// Портрет персонажа (если есть)
if (generatedImage != null)
{
float portraitSize = Mathf.Min(rect.width, 250f);
Rect portraitRect = new Rect(
rect.x + (rect.width - portraitSize) / 2f,
contentY,
portraitSize,
portraitSize
);
// Рамка вокруг портрета
Widgets.DrawBox(portraitRect);
GUI.DrawTexture(portraitRect.ContractedBy(2f), generatedImage);
contentY += portraitSize + 15f;
}
// Заголовок секции
Text.Font = GameFont.Medium;
Widgets.Label(
new Rect(rect.x, contentY, rect.width, 30f),
"AIImages.CharacterInfo.Title".Translate()
);
contentY += 35f;
// Разделитель
Widgets.DrawLineHorizontal(rect.x, contentY, rect.width);
contentY += 10f;
// Информация о персонаже
DrawCharacterInfoContent(rect, contentY);
}
///
/// Отрисовывает информацию о персонаже в компактном виде
///
private void DrawCharacterInfoContent(Rect parentRect, float startY)
{
float contentY = startY;
float lineHeight = 22f;
float labelWidth = parentRect.width * 0.45f;
float valueWidth = parentRect.width * 0.50f;
Text.Font = GameFont.Tiny;
// Базовая информация
var info = new[]
{
("AIImages.Info.Gender".Translate(), appearanceData.Gender.ToString()),
("AIImages.Info.Age".Translate(), appearanceData.Age.ToString()),
("AIImages.Info.BodyType".Translate(), appearanceData.BodyType),
(
"AIImages.Info.SkinTone".Translate(),
ColorDescriptionService.GetSkinToneDescription(appearanceData.SkinColor)
),
("AIImages.Info.Hair".Translate(), appearanceData.HairStyle),
(
"AIImages.Info.HairColor".Translate(),
ColorDescriptionService.GetHairColorDescription(appearanceData.HairColor)
),
};
foreach (var (label, value) in info)
{
// Подсветка строк через одну
var infoArray = info.ToArray();
int index = Array.IndexOf(infoArray, (label, value));
if ((index % 2) == 0)
{
Widgets.DrawBoxSolid(
new Rect(parentRect.x, contentY, parentRect.width, lineHeight),
new Color(0.15f, 0.15f, 0.15f, 0.3f)
);
}
Widgets.Label(
new Rect(parentRect.x + 5f, contentY, labelWidth, lineHeight),
label + ":"
);
Widgets.Label(
new Rect(parentRect.x + labelWidth + 10f, contentY, valueWidth, lineHeight),
value
);
contentY += lineHeight;
}
// Черты характера
if (pawn.story?.traits?.allTraits != null && pawn.story.traits.allTraits.Any())
{
contentY += 15f;
Text.Font = GameFont.Small;
Widgets.Label(
new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight),
"AIImages.Info.Traits".Translate() + ":"
);
contentY += lineHeight + 2f;
Text.Font = GameFont.Tiny;
var traitLabels = pawn.story.traits.allTraits.Select(trait => trait.LabelCap);
foreach (var traitLabel in traitLabels)
{
Widgets.Label(
new Rect(parentRect.x + 15f, contentY, parentRect.width - 20f, lineHeight),
"• " + traitLabel
);
contentY += lineHeight;
}
}
// Одежда
var apparel = pawn.apparel?.WornApparel;
if (apparel != null && apparel.Any())
{
contentY += 15f;
Text.Font = GameFont.Small;
Widgets.Label(
new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight),
"AIImages.Info.Apparel".Translate() + ":"
);
contentY += lineHeight + 2f;
Text.Font = GameFont.Tiny;
foreach (var item in apparel)
{
var colorDesc = ColorDescriptionService.GetApparelColorDescription(
item.DrawColor
);
string apparelLabel = $"• {colorDesc} {item.def.label}";
float apparelHeight = Text.CalcHeight(apparelLabel, parentRect.width - 25f);
Widgets.Label(
new Rect(
parentRect.x + 15f,
contentY,
parentRect.width - 25f,
apparelHeight
),
apparelLabel
);
contentY += apparelHeight;
}
}
}
private void DrawRightColumn(Rect rect)
{
float curY = rect.y;
curY = DrawImagePreview(rect, curY);
curY = DrawGenerationStatus(rect, curY);
curY = DrawProgressBar(rect, curY);
curY = DrawGenerationButton(rect, curY);
// Сворачиваемая секция с позитивным промптом
curY = DrawPromptSection(
rect,
curY,
"AIImages.Prompt.PositiveTitle".Translate(),
ref showPositivePrompt,
() =>
promptGeneratorService.GeneratePositivePrompt(
appearanceData,
generationSettings
),
new Color(0.1f, 0.3f, 0.1f, 0.5f),
ref promptScrollPosition
);
curY += 5f;
// Сворачиваемая секция с негативным промптом
curY = DrawPromptSection(
rect,
curY,
"AIImages.Prompt.NegativeTitle".Translate(),
ref showNegativePrompt,
() => promptGeneratorService.GenerateNegativePrompt(generationSettings),
new Color(0.3f, 0.1f, 0.1f, 0.5f),
ref negativePromptScrollPosition
);
curY += 10f;
// Кнопка обновления данных
if (
Widgets.ButtonText(
new Rect(rect.x, curY, rect.width, 30f),
"AIImages.Window.Refresh".Translate()
)
)
{
RefreshPawnData();
}
// Сообщение о копировании
if (copiedMessageTime > 0f)
{
curY += 35f;
GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f);
Widgets.Label(
new Rect(rect.x, curY, rect.width, 25f),
"AIImages.Prompt.Copied".Translate()
);
GUI.color = Color.white;
}
}
private float CalculateContentHeight()
{
float height = 0f;
// Портрет персонажа (если есть)
if (generatedImage != null)
{
float portraitSize = 250f; // Максимальный размер портрета
height += portraitSize + 15f;
}
// Заголовок "Информация о персонаже"
height += 35f;
// Разделитель
height += 10f;
// Базовая информация (6 строк по 22px)
height += 6 * 22f;
// Черты характера (если есть)
if (pawn.story?.traits?.allTraits != null && pawn.story.traits.allTraits.Any())
{
height += 15f; // Отступ
height += 22f; // Заголовок "Черты характера"
height += 2f; // Отступ
height += pawn.story.traits.allTraits.Count * 22f; // Каждая черта
}
// Одежда (если есть)
var apparel = pawn.apparel?.WornApparel;
if (apparel != null && apparel.Any())
{
height += 15f; // Отступ
height += 22f; // Заголовок "Одежда"
height += 2f; // Отступ
// Примерно по 22-30px на предмет одежды
height += apparel.Count * 26f;
}
// Дополнительный отступ
height += 50f;
return height;
}
private float CalculateRightColumnHeight()
{
float height = 0f;
// Превью изображения
if (generatedImage != null)
{
height += 410f; // 400f изображение + 10f отступ
}
else if (!isGenerating)
{
height += 310f; // 300f placeholder + 10f отступ
}
// Статус генерации
if (!string.IsNullOrEmpty(generationStatus))
{
height += 30f;
}
// Прогресс бар
if (isGenerating && generationProgress > 0.0)
{
height += 30f;
}
// Кнопка генерации
height += 40f;
// Позитивный промпт
height += 36f; // Заголовок (32f + 4f отступ)
if (showPositivePrompt)
{
height += 140f; // Бокс развернут (120f + 10f отступ + запас)
}
height += 5f; // Отступ между промптами
// Негативный промпт
height += 36f; // Заголовок (32f + 4f отступ)
if (showNegativePrompt)
{
height += 140f; // Бокс развернут (120f + 10f отступ + запас)
}
height += 10f; // Отступ после промптов
// Кнопка обновления
height += 40f; // Увеличено с 35f до 40f
// Сообщение о копировании
if (copiedMessageTime > 0f)
{
height += 40f;
}
return height + 100f; // Увеличен дополнительный отступ снизу
}
private float CalculateTotalContentHeight()
{
float height = 0f;
// Заголовок и имя пешки
height += 45f + 40f;
// Разделитель
height += 10f;
// Высота колонок (берем большую)
float columnHeight = Mathf.Max(CalculateContentHeight(), CalculateRightColumnHeight());
height += columnHeight;
// Дополнительный отступ снизу
height += 30f;
return height;
}
///
/// /// Отрисовывает секцию с промптом
///
private float DrawPromptSection(
Rect parentRect,
float startY,
string title,
ref bool expanded,
System.Func getPromptFunc,
Color backgroundColor,
ref Vector2 scrollPosition
)
{
float curY = startY;
float headerHeight = 32f;
// Получаем промпт
string prompt = getPromptFunc();
// Рисуем заголовок с фоном
Rect headerRect = new Rect(parentRect.x, curY, parentRect.width, headerHeight);
Widgets.DrawBoxSolid(headerRect, new Color(0.25f, 0.25f, 0.25f, 0.8f));
Widgets.DrawBox(headerRect);
// Иконка раскрытия
string icon = expanded ? "▼" : "►";
Text.Font = GameFont.Small;
Widgets.Label(new Rect(parentRect.x + 8f, curY + 6f, 20f, headerHeight), icon);
// Заголовок
Widgets.Label(
new Rect(parentRect.x + 30f, curY + 6f, parentRect.width - 110f, headerHeight),
title
);
// Кнопка копирования в заголовке (увеличена ширина)
Rect copyButtonRect = new Rect(
parentRect.x + parentRect.width - 100f,
curY + 4f,
95f,
24f
);
if (Widgets.ButtonText(copyButtonRect, "📋 " + "AIImages.Copy".Translate()))
{
GUIUtility.systemCopyBuffer = prompt;
copiedMessageTime = 2f;
}
// Клик на остальной области для раскрытия/сворачивания
Rect clickableHeaderRect = new Rect(
parentRect.x,
curY,
parentRect.width - 105f,
headerHeight
);
if (Widgets.ButtonInvisible(clickableHeaderRect))
{
expanded = !expanded;
}
curY += headerHeight + 4f;
// Рисуем содержимое если развернуто
if (expanded)
{
float promptBoxHeight = 120f;
Text.Font = GameFont.Tiny;
float actualPromptHeight = Text.CalcHeight(prompt, parentRect.width - 20f);
Rect promptOuterRect = new Rect(
parentRect.x,
curY,
parentRect.width,
promptBoxHeight
);
Rect promptViewRect = new Rect(0f, 0f, parentRect.width - 20f, actualPromptHeight);
// Рисуем фон
Widgets.DrawBoxSolid(promptOuterRect, backgroundColor);
Widgets.DrawBox(promptOuterRect);
// Рисуем промпт с прокруткой
Widgets.BeginScrollView(
promptOuterRect.ContractedBy(5f),
ref scrollPosition,
promptViewRect
);
var prevAnchor = Text.Anchor;
Text.Anchor = TextAnchor.UpperLeft;
Text.WordWrap = true;
Widgets.Label(
new Rect(0f, 0f, promptViewRect.width, promptViewRect.height),
prompt
);
Text.Anchor = prevAnchor;
Text.WordWrap = true;
Widgets.EndScrollView();
curY += promptBoxHeight + 10f;
}
return curY;
}
private float DrawImagePreview(Rect rect, float curY)
{
if (generatedImage != null)
{
float imageSize = Mathf.Min(rect.width, 400f);
Rect imageRect = new Rect(
rect.x + (rect.width - imageSize) / 2f,
curY,
imageSize,
imageSize
);
GUI.DrawTexture(imageRect, generatedImage);
return curY + imageSize + 10f;
}
float placeholderSize = Mathf.Min(rect.width, 300f);
Rect placeholderRect = new Rect(
rect.x + (rect.width - placeholderSize) / 2f,
curY,
placeholderSize,
placeholderSize
);
Widgets.DrawBoxSolid(placeholderRect, new Color(0.2f, 0.2f, 0.2f));
Text.Anchor = TextAnchor.MiddleCenter;
Widgets.Label(placeholderRect, "AIImages.Generation.NoImage".Translate());
Text.Anchor = TextAnchor.UpperLeft;
return curY + placeholderSize + 10f;
}
private float DrawGenerationStatus(Rect rect, float curY)
{
if (string.IsNullOrEmpty(generationStatus))
return curY;
Text.Font = GameFont.Small;
float statusHeight = Text.CalcHeight(generationStatus, rect.width);
Widgets.Label(new Rect(rect.x, curY, rect.width, statusHeight), generationStatus);
return curY + statusHeight + 10f;
}
private float DrawProgressBar(Rect rect, float curY)
{
if (!isGenerating || generationProgress <= 0.0)
return curY;
Rect progressBarRect = new Rect(rect.x, curY, rect.width, 24f);
string progressText;
if (totalSteps > 0)
{
progressText =
$"{(generationProgress * 100):F1}% - Step {currentStep}/{totalSteps}";
if (etaSeconds > 0)
{
progressText += $" - ETA: {etaSeconds:F0}s";
}
}
else
{
progressText = $"{(generationProgress * 100):F1}%";
}
Widgets.FillableBar(progressBarRect, (float)generationProgress);
Text.Font = GameFont.Tiny;
Text.Anchor = TextAnchor.MiddleCenter;
Widgets.Label(progressBarRect, progressText);
Text.Anchor = TextAnchor.UpperLeft;
return curY + 30f;
}
private float DrawGenerationButton(Rect rect, float curY)
{
Text.Font = GameFont.Small;
string buttonLabel = isGenerating
? "AIImages.Generation.Cancel".Translate()
: "AIImages.Generation.Generate".Translate();
// Основная кнопка генерации (занимает 70% ширины)
float buttonWidth = rect.width * 0.7f;
if (Widgets.ButtonText(new Rect(rect.x, curY, buttonWidth, 35f), buttonLabel))
{
if (isGenerating)
CancelGeneration();
else
StartGeneration();
}
// Отладочная кнопка (занимает 25% ширины)
float debugButtonWidth = rect.width * 0.25f;
float debugButtonX = rect.x + buttonWidth + 10f;
if (Widgets.ButtonText(new Rect(debugButtonX, curY, debugButtonWidth, 35f), "Debug"))
{
DebugAllPawns();
}
return curY + 40f;
}
}
}