Окно

Ранее Мы получили некоторую мотивацию для использования Rust и настроили нашу среду разработки.

В этой главе мы создадим окно! Если это не звучит слишком увлекательно, мы также узнаем о пакетах Rust. Очень волнующе.

Чтобы создать окно, которое работает на нескольких платформах, а также использовать OpenGL ES контекст или кросс-платформенный ввод пользователя мы будем использовать пакет winit.

Библиотеки Rust

Библиотеки Rust называются crates, а консольный менеджер пакетов для Rust называется cargo. Библиотеки могут быть найдены на центральном репозитории по ссылке crates.io.

Новуй пакет для Rust можно создать в используя команду cargo new library-name. Структура каталогов для новой пакета будет выглядеть очень похожэ на наш первый проект hello-world:

library-name
    src
        lib.rs
    Cargo.toml

Разница от исполняемого проекта, в том что в директории src вместо main.rs присутствует lib.rs.

Файл Cargo.toml описывает наш проект, в независимоти от того библиотека это, или исполняемый проект. Он содержит идентификатор преокта, список зависимостей, ссылку на документацию и многое другое. За более детальной информацией по содержимому манифеста Cargo.toml обратитесь к документации по Cargo.

Пакет winit

Пакет winit это кросс-платформенная библиотека для создания окна и обработки событий пользователя, таких как ввод пользователя с клавиатуры или мышки.

Можно посмотерть более детальную информацию об этом пакете по ссылке winit.

Зависимости

Для того чтобы быстро довавлять зависимости в наш проект мы будем использовать cargo-edit. Для этого нужно его установить с помошью команды в вашем терминале:

> cargo install cargo-edit

Давайте создадим новый проект по имени game.

> cargo new --bin game

Чтобы добавить пакет winit в наш проект введите команду в терминале из корня проекта:

> cargo add winit

Эта команда добавит секцию [dependencies] в файл Cargo.toml, с пакетом winit в качестве зависимости:

Cargo.toml, incomplete

[dependencies]
winit = "0.25.0"

На момент написания этой главы новейшей версий winit является 0.25.0. Вы можете ввести эту версию, чтобы убедиться, что все компилируется.

Указание зависимости позволяет загрузить её.

Чуть ниже я объясню назначение остальных зависимостей, сейчас же приведите секцию [dependencies] к следующему виду:

[dependencies]
env_logger = "0.8"
khronos-egl = "4.1"
log = "0.4"
opengles = "0.1"
raw-window-handle = "0.3"
winit = "0.25"

Чтобы использовать заши зависимости, мы должны ссылаться на них.

Использование winit

Укажите ссылки на неообходимые нам структуры, добавив в верхнюю часть файла main.rs соответствующий код:


#![allow(unused)]
fn main() {
use winit::{
    dpi::{LogicalSize, PhysicalSize, Size},
    event::{Event, KeyboardInput, VirtualKeyCode, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
    window::WindowBuilder,
};
}

Этот позволит использовать короткие имена структур из пакета winit. Функции winit могут быть вызваны как EventLoop::new().

Инициализация winit.

Приведите код функции main к виду показаному ниже:

fn main() {
    let event_loop = EventLoop::new();

    let wb = WindowBuilder::new()
        .with_min_inner_size(Size::Logical(LogicalSize::new(64.0, 64.0)))
        .with_inner_size(Size::Physical(PhysicalSize::new(900, 700)))
        .with_title("Game".to_string());

    let _window = wb.build(&event_loop).unwrap();

    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

        match event {
            Event::WindowEvent { event, .. } => match event {
                WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
                WindowEvent::KeyboardInput {
                    input:
                        KeyboardInput {
                            virtual_keycode: Some(VirtualKeyCode::Escape),
                            ..
                        },
                    ..
                } => *control_flow = ControlFlow::Exit,
                WindowEvent::Resized(_) => {
                    // make changes based on window size
                }
                _ => {}
            },
            Event::RedrawEventsCleared => {
                // render window contents here
            }
            _ => {}
        }
    });
}

Ниже мы обсудим каждую деталь кода. Но сначала запустим!

> cargo run

Возможно вы не сразу заметите созданное окно, потому что мы ничего не отображали в нем.

   Compiling lesson-01-window v0.1.0 (/home/opengles-tutorial)
    Finished dev [unoptimized + debuginfo] target(s) in 9.31s
     Running `/home/opengles-tutorial/target/debug/lesson-01-window`

Прервите исполнение с помощью комбинации клавишь Ctrl+C.

Разбор кода

Вернемся к коду. Давайте разберем его по крупицам.


#![allow(unused)]
fn main() {
let event_loop = EventLoop::new();
}

Эта строчка инициализирует цикл событий для обработки событий вашей оконной системы, таких как запрос на перерисовку окна или ввод пользователя. До тех пор пока работает цикл обработки событий работает и ваше приложение.


#![allow(unused)]
fn main() {
let wb = WindowBuilder::new()
    .with_min_inner_size(Size::Logical(LogicalSize::new(64.0, 64.0)))
    .with_inner_size(Size::Physical(PhysicalSize::new(900, 700)))
    .with_title("Game".to_string());
}

Строчки выше конфигурируют окно используя Builder паттерн. Мы задаем минимальный рамер окна, текущий размер окна и заголовок.


#![allow(unused)]
fn main() {
let _window = wb.build(&event_loop).unwrap();
}

Мы создаем окно, привязав его к циклу обработки событий оконной системы. Мы использовали имя переменной с префиксом поддеркивания _window, чтобы избежать предупреждающих сообщений компилятора, потому как мы еще не используем методы окна.


#![allow(unused)]
fn main() {
event_loop.run(move |event, _, control_flow| {
    ...
});
}

Конструкция выше запускает основной цикл обработки событий оконной системы. Программа работает до тех пор пока мы находимся в этом цикле. Единственным аргументом этого цикла является обработчик событий в виде замыкания.

Рассмотрим процесс обработки событий более детально. Обработчик событий помимо непосредственно события получает ссылку на объект, который контролирует цикл обработки событий.


#![allow(unused)]
fn main() {
*control_flow = ControlFlow::Wait;
}

Установив его в значение ControlFlow::Wait мы приостанавливаем цикл обработки событий, если события недоступны для обработки. Это идеально подходит для неигровых приложений, которые обновляются только в ответ на ввод пользователя и потребляют значительно меньше энергии/времени процессора, чем ControlFlow::Poll.

Непосредственная обработка события подразумевает собой разбор перечисления события:


#![allow(unused)]
fn main() {
match event {
    Event::WindowEvent { event, .. } => match event {
        ...
    },
    Event::RedrawEventsCleared => {
        // render window contents here
    }
    _ => {}
}
}

В случае Event::WindowEvent мы обрабатываем события оконного менеджера и ввод пользователя. Когда мы получаем Event::RedrawEventsCleared, то мы должны отрисовать содержимое окна. Здесь мы будем рендерить нашу графику.

Нам осталось более детально рассмотреть обработку событий окна.


#![allow(unused)]
fn main() {
Event::WindowEvent { event, .. } => match event {
    WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
    WindowEvent::KeyboardInput {
        input:
            KeyboardInput {
                virtual_keycode: Some(VirtualKeyCode::Escape),
                ..
            },
        ..
    } => *control_flow = ControlFlow::Exit,
    WindowEvent::Resized(_) => {
        // make changes based on window size
    }
    _ => {}
},
}

WindowEvent::CloseRequested происходит в случае закрытия окна с помощью x кнопки окна. Логичной реакцией на это действие - будет завершение работы программы. Чтобы это сделать мы присвоим переменной состояния цикла обработки событий значение ControlFlow::Exit.

Обработка событий клавиатуры происходит по событию WindowEvent::KeyboardInput. В этой ситуации если пользователь нажмет клавишу Esc, мы также завершим работу программы.

Последнее событие котрое мы будем обрабатывать - WindowEvent::Resized. Оно отвечает за изменение размеров окна. И в этой ситуации я предпологаю что мы должны поменять внутренние переменные нашего приложения для корректного отображения нашей графики.

Остальные события на данном этапе мы не обрабатываем. Чтобы получить более детальную информацию по доступным событиям обратитесь к документации winit

Код этой главы доступен в основном репозитории книги.

Теперь наше окно может быть окончательно закрыто, и мы можем начать рисовать внутри окна.