Documentation

Overview

Π’ этом Ρ€Π°Π·Π΄Π΅Π»Π΅ прСдставлСн ΠΎΠ±Π·ΠΎΡ€ систСмы Π²Π΅Π±-интСрфСйсов Wudgine, которая позволяСт ΡΠΎΠ·Π΄Π°Π²Π°Ρ‚ΡŒ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΠ΅ интСрфСйсы с использованиСм HTML, CSS ΠΈ JavaScript.

Π’Π²Π΅Π΄Π΅Π½ΠΈΠ΅

Wudgine прСдоставляСт ΠΌΠΎΡ‰Π½ΡƒΡŽ систСму Π²Π΅Π±-интСрфСйсов, ΠΎΡΠ½ΠΎΠ²Π°Π½Π½ΡƒΡŽ Π½Π° Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ΅ Ultralight, которая позволяСт Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ°ΠΌ ΡΠΎΠ·Π΄Π°Π²Π°Ρ‚ΡŒ ΠΈΠ³Ρ€ΠΎΠ²Ρ‹Π΅ интСрфСйсы с использованиСм стандартных Π²Π΅Π±-Ρ‚Π΅Ρ…Π½ΠΎΠ»ΠΎΠ³ΠΈΠΉ.

ИспользованиС Π²Π΅Π±-Ρ‚Π΅Ρ…Π½ΠΎΠ»ΠΎΠ³ΠΈΠΉ для UI позволяСт Π·Π½Π°Ρ‡ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ ΡƒΡΠΊΠΎΡ€ΠΈΡ‚ΡŒ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΡƒ интСрфСйсов ΠΈ ΠΏΡ€ΠΈΠ²Π»Π΅ΠΊΠ°Ρ‚ΡŒ Π²Π΅Π±-Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ² ΠΊ созданию ΠΈΠ³Ρ€ΠΎΠ²ΠΎΠ³ΠΎ UI.

АрхитСктура Π²Π΅Π±-подсистСмы

БистСма Π²Π΅Π±-интСрфСйсов Wudgine состоит ΠΈΠ· ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΡ… основных ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ²:

  • WebView: Основной ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ для Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° Π²Π΅Π±-содСрТимого
  • JavaScript Bridge: ΠœΠΎΡΡ‚ ΠΌΠ΅ΠΆΠ΄Ρƒ C++ ΠΈ JavaScript для двустороннСй ΠΊΠΎΠΌΠΌΡƒΠ½ΠΈΠΊΠ°Ρ†ΠΈΠΈ
  • WebUI Component: ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ ECS для ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ Π²Π΅Π±-интСрфСйсов Π² ΠΈΠ³Ρ€ΠΎΠ²Ρ‹Π΅ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹
  • WebUI System: БистСма ECS для обновлСния ΠΈ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° Π²Π΅Π±-интСрфСйсов
  • WebUI Resource: РСсурс для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ ΠΈ управлСния Π²Π΅Π±-содСрТимым

ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ возмоТности

Π Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³ HTML/CSS

Wudgine ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ Π±ΠΎΠ»ΡŒΡˆΠΈΠ½ΡΡ‚Π²ΠΎ соврСмСнных стандартов HTML5 ΠΈ CSS3, Π²ΠΊΠ»ΡŽΡ‡Π°Ρ:

  • Flexbox ΠΈ Grid для создания Π°Π΄Π°ΠΏΡ‚ΠΈΠ²Π½Ρ‹Ρ… ΠΌΠ°ΠΊΠ΅Ρ‚ΠΎΠ²
  • CSS-Π°Π½ΠΈΠΌΠ°Ρ†ΠΈΠΈ ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄Ρ‹
  • Π¨Ρ€ΠΈΡ„Ρ‚Ρ‹ ΠΈ ΠΈΠΊΠΎΠ½ΠΊΠΈ
  • SVG-Π³Ρ€Π°Ρ„ΠΈΠΊΠ°
  • МСдиа-запросы для Π°Π΄Π°ΠΏΡ‚ΠΈΠ²Π½ΠΎΠ³ΠΎ Π΄ΠΈΠ·Π°ΠΉΠ½Π°
<!-- ΠŸΡ€ΠΈΠΌΠ΅Ρ€ HTML-структуры для ΠΈΠ³Ρ€ΠΎΠ²ΠΎΠ³ΠΎ интСрфСйса -->
<div class="game-ui">
  <header class="top-bar">
    <div class="health-container">
      <div class="health-bar" style="width: 75%"></div>
    </div>
    <div class="resources">
      <span class="gold">1250</span>
      <span class="gems">7</span>
    </div>
  </header>
  
  <div class="inventory-grid">
    <!-- Π―Ρ‡Π΅ΠΉΠΊΠΈ инвСнтаря Π±ΡƒΠ΄ΡƒΡ‚ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹ Ρ‡Π΅Ρ€Π΅Π· JavaScript -->
  </div>
  
  <footer class="action-bar">
    <button class="action-button" data-action="attack">Атака</button>
    <button class="action-button" data-action="defend">Π—Π°Ρ‰ΠΈΡ‚Π°</button>
    <button class="action-button" data-action="special">ΠžΡΠΎΠ±Ρ‹ΠΉ Π½Π°Π²Ρ‹ΠΊ</button>
  </footer>
</div>
/* ΠŸΡ€ΠΈΠΌΠ΅Ρ€ CSS для ΠΈΠ³Ρ€ΠΎΠ²ΠΎΠ³ΠΎ интСрфСйса */
.game-ui {
  display: flex;
  flex-direction: column;
  height: 100%;
  font-family: 'GameFont', sans-serif;
  color: #e0e0e0;
}

.top-bar {
  display: flex;
  justify-content: space-between;
  padding: 10px;
  background: rgba(0, 0, 0, 0.7);
}

.health-container {
  width: 200px;
  height: 20px;
  background: #333;
  border: 2px solid #555;
  border-radius: 10px;
  overflow: hidden;
}

.health-bar {
  height: 100%;
  background: linear-gradient(to right, #f00, #f55);
  transition: width 0.3s ease-out;
}

.inventory-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 10px;
  padding: 20px;
  flex-grow: 1;
}

.inventory-slot {
  aspect-ratio: 1;
  background: rgba(0, 0, 0, 0.5);
  border: 2px solid #555;
  border-radius: 5px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}

.action-bar {
  display: flex;
  justify-content: space-around;
  padding: 10px;
  background: rgba(0, 0, 0, 0.7);
}

.action-button {
  padding: 10px 20px;
  background: linear-gradient(to bottom, #555, #333);
  border: 2px solid #777;
  border-radius: 5px;
  color: #e0e0e0;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.2s;
}

.action-button:hover {
  background: linear-gradient(to bottom, #666, #444);
  transform: scale(1.05);
}

.action-button:active {
  background: linear-gradient(to bottom, #444, #222);
  transform: scale(0.95);
}

JavaScript-интСграция

Wudgine прСдоставляСт Π΄Π²ΡƒΡΡ‚ΠΎΡ€ΠΎΠ½Π½ΡŽΡŽ ΠΊΠΎΠΌΠΌΡƒΠ½ΠΈΠΊΠ°Ρ†ΠΈΡŽ ΠΌΠ΅ΠΆΠ΄Ρƒ C++ ΠΈ JavaScript:

// РСгистрация C++ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ для Π²Ρ‹Π·ΠΎΠ²Π° ΠΈΠ· JavaScript
auto& webUI = entity.getComponent<WebUIComponent>();
webUI.registerFunction("addItemToInventory", [](const json& args) -> json {
    int itemId = args["itemId"];
    std::string itemName = args["name"];
    
    // Π›ΠΎΠ³ΠΈΠΊΠ° добавлСния ΠΏΡ€Π΅Π΄ΠΌΠ΅Ρ‚Π° Π² ΠΈΠ½Π²Π΅Π½Ρ‚Π°Ρ€ΡŒ
    InventoryManager::getInstance().addItem(itemId, itemName);
    
    // Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ Π² JavaScript
    return {
        {"success", true},
        {"newCount", InventoryManager::getInstance().getItemCount(itemId)}
    };
});

// Π’Ρ‹Π·ΠΎΠ² JavaScript-Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ ΠΈΠ· C++
json result = webUI.callJavaScriptFunction("updateInventoryUI", {
    {"items", InventoryManager::getInstance().getAllItems()},
    {"maxSlots", 25}
});

bool success = result["success"];

Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с ECS

Wudgine прСдоставляСт ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ WebUIComponent для ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ Π²Π΅Π±-интСрфСйсов Π² сущности ECS:

// Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ сущности с Π²Π΅Π±-интСрфСйсом
Entity uiEntity = world.createEntity("InventoryUI");

// Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π° WebUI
auto& webUI = uiEntity.addComponent<WebUIComponent>();
webUI.setSource("assets/ui/inventory.html");
webUI.setSize(800, 600);
webUI.setTransparent(true);
webUI.setInteractive(true);

// Настройка ΠΏΠΎΠ·ΠΈΡ†ΠΈΠΈ ΠΈ Ρ€Π°Π·ΠΌΠ΅Ρ€Π° Π² 3D-пространствС
auto& transform = uiEntity.getComponent<TransformComponent>();
transform.position = Vector3(0.0f, 1.7f, 0.5f);
transform.rotation = Quaternion::fromEuler(0.0f, 180.0f, 0.0f);
transform.scale = Vector3(0.002f, 0.002f, 0.002f);

// РСгистрация ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ² событий
webUI.onLoadComplete([&uiEntity]() {
    Debug::log("UI Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½: {}", uiEntity.getName());
    
    // Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ UI послС Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ
    auto& webUI = uiEntity.getComponent<WebUIComponent>();
    webUI.callJavaScriptFunction("initializeUI", {
        {"playerName", "Π“Π΅Ρ€ΠΎΠΉ"},
        {"level", 5},
        {"maxHealth", 100},
        {"currentHealth", 75}
    });
});

ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ использования

Π’Π½ΡƒΡ‚Ρ€ΠΈΠΈΠ³Ρ€ΠΎΠ²ΠΎΠΉ HUD

// Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ HUD
Entity hud = world.createEntity("PlayerHUD");
auto& webUI = hud.addComponent<WebUIComponent>();
webUI.setSource("assets/ui/hud.html");
webUI.setSize(1920, 1080);
webUI.setFullscreen(true);
webUI.setTransparent(true);
webUI.setInteractive(false);

// БистСма для обновлСния HUD
class HUDSystem : public ISystem {
private:
    World* m_world;
    Entity m_player;
    Entity m_hud;

public:
    HUDSystem(World* world) : m_world(world) {
        m_player = world->findEntityByName("Player");
        m_hud = world->findEntityByName("PlayerHUD");
    }

    void update(float deltaTime) override {
        if (!m_player.isValid() || !m_hud.isValid()) return;
        
        auto& health = m_player.getComponent<HealthComponent>();
        auto& inventory = m_player.getComponent<InventoryComponent>();
        auto& webUI = m_hud.getComponent<WebUIComponent>();
        
        // ОбновлСниС HUD
        webUI.callJavaScriptFunction("updateHUD", {
            {"health", health.currentHealth},
            {"maxHealth", health.maxHealth},
            {"stamina", m_player.getComponent<StaminaComponent>().currentStamina},
            {"maxStamina", m_player.getComponent<StaminaComponent>().maxStamina},
            {"gold", inventory.gold},
            {"ammo", inventory.getAmmoCount()}
        });
    }
};

Π˜Π½Π²Π΅Π½Ρ‚Π°Ρ€ΡŒ ΠΈ мСню

// Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ инвСнтаря
Entity inventoryUI = world.createEntity("InventoryUI");
auto& webUI = inventoryUI.addComponent<WebUIComponent>();
webUI.setSource("assets/ui/inventory.html");
webUI.setSize(1200, 800);
webUI.setVisible(false); // Π˜Π·Π½Π°Ρ‡Π°Π»ΡŒΠ½ΠΎ скрыт
webUI.setInteractive(true);

// РСгистрация ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ² событий
webUI.registerFunction("useItem", [&inventoryUI](const json& args) -> json {
    int itemId = args["itemId"];
    
    // ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ΠΈΠ³Ρ€ΠΎΠΊΠ°
    Entity player = world.findEntityByName("Player");
    if (!player.isValid()) {
        return {{"success", false}, {"error", "Player not found"}};
    }
    
    // ИспользованиС ΠΏΡ€Π΅Π΄ΠΌΠ΅Ρ‚Π°
    bool success = player.getComponent<InventoryComponent>().useItem(itemId);
    
    return {{"success", success}};
});

// БистСма для открытия/закрытия инвСнтаря
class InventoryUISystem : public ISystem {
private:
    World* m_world;
    Entity m_inventoryUI;
    InputManager* m_inputManager;

public:
    InventoryUISystem(World* world, InputManager* inputManager) 
        : m_world(world), m_inputManager(inputManager) {
        m_inventoryUI = world->findEntityByName("InventoryUI");
        
        // РСгистрация ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ° клавиши
        m_inputManager->registerKeyCallback(KeyCode::I, [this](bool pressed) {
            if (pressed) {
                toggleInventory();
            }
        });
    }

    void toggleInventory() {
        if (!m_inventoryUI.isValid()) return;
        
        auto& webUI = m_inventoryUI.getComponent<WebUIComponent>();
        bool isVisible = webUI.isVisible();
        
        webUI.setVisible(!isVisible);
        
        if (!isVisible) {
            // ОбновлСниС Π΄Π°Π½Π½Ρ‹Ρ… ΠΏΡ€ΠΈ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΈΠΈ инвСнтаря
            Entity player = m_world->findEntityByName("Player");
            if (player.isValid()) {
                auto& inventory = player.getComponent<InventoryComponent>();
                webUI.callJavaScriptFunction("updateInventoryUI", {
                    {"items", inventory.getAllItems()},
                    {"maxSlots", inventory.getMaxSlots()}
                });
            }
        }
    }
};

ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΡ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ

Для ΠΎΠΏΡ‚ΠΈΠΌΠ°Π»ΡŒΠ½ΠΎΠΉ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ Π²Π΅Π±-интСрфСйсов слСдуйтС этим рСкомСндациям:

  • ΠœΠΈΠ½ΠΈΠΌΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ количСство DOM-элСмСнтов
  • Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ CSS-Π°Π½ΠΈΠΌΠ°Ρ†ΠΈΠΈ вмСсто JavaScript-Π°Π½ΠΈΠΌΠ°Ρ†ΠΈΠΉ
  • Π˜Π·Π±Π΅Π³Π°ΠΉΡ‚Π΅ слоТных сСлСкторов CSS
  • ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ изобраТСния ΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ спрайты
  • ΠœΠΈΠ½ΠΈΠΌΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ количСство Π²Ρ‹Π·ΠΎΠ²ΠΎΠ² ΠΌΠ΅ΠΆΠ΄Ρƒ C++ ΠΈ JavaScript
  • Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ ΠΏΡƒΠ»ΠΈΠ½Π³ для часто создаваСмых элСмСнтов
// ΠŸΡ€ΠΈΠΌΠ΅Ρ€ ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ с использованиСм ΠΏΡƒΠ»ΠΈΠ½Π³Π° элСмСнтов
class ElementPool {
    constructor(tagName, className, parent, initialCount = 10) {
        this.pool = [];
        this.parent = parent;
        
        // Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Π½Π°Ρ‡Π°Π»ΡŒΠ½ΠΎΠ³ΠΎ ΠΏΡƒΠ»Π° элСмСнтов
        for (let i = 0; i < initialCount; i++) {
            const element = document.createElement(tagName);
            element.className = className;
            element.style.display = 'none';
            parent.appendChild(element);
            this.pool.push(element);
        }
    }
    
    get() {
        // ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ элСмСнта ΠΈΠ· ΠΏΡƒΠ»Π° ΠΈΠ»ΠΈ созданиС Π½ΠΎΠ²ΠΎΠ³ΠΎ
        let element;
        if (this.pool.length > 0) {
            element = this.pool.pop();
        } else {
            element = document.createElement(tagName);
            element.className = className;
            this.parent.appendChild(element);
        }
        
        element.style.display = '';
        return element;
    }
    
    release(element) {
        // Π’ΠΎΠ·Π²Ρ€Π°Ρ‚ элСмСнта Π² ΠΏΡƒΠ»
        element.style.display = 'none';
        this.pool.push(element);
    }
}

// ИспользованиС ΠΏΡƒΠ»Π° для создания частиц
const particleContainer = document.querySelector('.particle-container');
const particlePool = new ElementPool('div', 'particle', particleContainer, 50);

function createParticle(x, y) {
    const particle = particlePool.get();
    particle.style.left = `${x}px`;
    particle.style.top = `${y}px`;
    
    // Анимация частицы
    setTimeout(() => {
        particlePool.release(particle);
    }, 1000);
}

ΠžΡ‚Π»Π°Π΄ΠΊΠ° Π²Π΅Π±-интСрфСйсов

Wudgine прСдоставляСт инструмСнты для ΠΎΡ‚Π»Π°Π΄ΠΊΠΈ Π²Π΅Π±-интСрфСйсов:

  • ВстроСнная консоль JavaScript
  • Π˜Π½ΡΠΏΠ΅ΠΊΡ‚ΠΎΡ€ DOM-элСмСнтов
  • ΠžΡ‚Π»Π°Π΄ΠΊΠ° сСтСвых запросов
  • ΠŸΡ€ΠΎΡ„ΠΈΠ»ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ
// Π’ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ инструмСнтов Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ° для Π²Π΅Π±-интСрфСйса
auto& webUI = entity.getComponent<WebUIComponent>();
webUI.enableDevTools(true);

// Π’Ρ‹Π²ΠΎΠ΄ сообщСний Π² консоль JavaScript
webUI.executeJavaScript("console.log('ΠžΡ‚Π»Π°Π΄ΠΎΡ‡Π½ΠΎΠ΅ сообщСниС');");

// ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ошибок JavaScript
webUI.onJavaScriptError([](const std::string& errorMessage, 
                          const std::string& sourceFile, 
                          int lineNumber) {
    Debug::logError("JavaScript Error: {} in {} at line {}", 
                   errorMessage, sourceFile, lineNumber);
});

Π‘Π»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠ΅ шаги

Π’Π΅ΠΏΠ΅Ρ€ΡŒ, ΠΊΠΎΠ³Π΄Π° Π²Ρ‹ ознакомились с основами Π²Π΅Π±-интСрфСйсов Π² Wudgine, Ρ€Π΅ΠΊΠΎΠΌΠ΅Π½Π΄ΡƒΠ΅ΠΌ:

JavaScript API

Π˜Π·ΡƒΡ‡ΠΈΡ‚Π΅ JavaScript API для взаимодСйствия с Π΄Π²ΠΈΠΆΠΊΠΎΠΌ.

UI ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹

Π£Π·Π½Π°ΠΉΡ‚Π΅ ΠΎ Π³ΠΎΡ‚ΠΎΠ²Ρ‹Ρ… UI ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°Ρ… ΠΈ ΡˆΠ°Π±Π»ΠΎΠ½Π°Ρ….

ΠŸΠ»Π°Π³ΠΈΠ½Ρ‹

Π˜Π·ΡƒΡ‡ΠΈΡ‚Π΅ систСму ΠΏΠ»Π°Π³ΠΈΠ½ΠΎΠ² Wudgine.

Wudgine β€’ Β© 2025