Перейти к содержанию

UI приложение на Druid

Posted on:5 февраля 2023 г. at 21:08

Example Dynamic OG Image link В текущее время растет популярность flutter разработанного компанией google под язык dart. Многим разработчикам нравится простота работы в flutter с виджетами, но этот UI фрамеворк не одинок в своей идее, виджеты используются во многих UI библиотеках с ранних времен. Rust так же не обошло это стороной, Raph Levien и Colin Rofls разрабатывают в настоящее время простой графический интерфейс, ориентированный на виджеты - druid.

Данная библиотека позволяет создавать простые интерактивные графические приложения, которые могут быть развернуты на Windows, macOS, Linux, OpenBSD, FreeBSD и в web.

Сам druid построен поверх druid-shell, которая реализует весь код нижнего уровня, специфичный для платформы, обеспечивая общую абстракцию для таких вещей, как события клавиш и мыши, создание окон и запуск приложения. Ниже druid-shell находится piet, представляющий собой кроссплатформенную библиотеку 2D-графики, простой и знакомый API для рисования, который может быть реализован так же для различных платформ.

Давайте создадим простое приложение, что бы попробовать библиотеку на “вкус”. Для этого нам понадобится компилятор rust и любой текстовый редактор. Я предпологаю, что вы уже знаете основы языка rust и не буду останавливаться на основах.

Инициализируем приложение:

cargo new exsample-druid

Это очень похоже на npm install <приложение> из мира node.js, неправда ли?

Добавим после всего, в самый низ файла Cargo.toml следующиее:

[dependencies.druid]
version = "0.8.2"
features = ["im", "svg", "image"]

Версия может отличаться в будущем, посмотрите актуальную версию у разработчиков druid. Думаю мало что изменится для нашего примера в более новых сборках.

Для начала подключим один виджет и несколько необходимых функций

use druid::widget::{Flex};
use druid::{WindowDesc, Widget, Data, AppLauncher};

и создадим структуру, описывающую состояние виджета (компонента), которое нам пригодится в будущем. Указав что это клонируемые данные, плюс добавим атрибут друида Data.

#[derive(Clone, Data)]
struct CountState {
    count: i32
}

Подготовим и сам минимальный виджет, который является виджетом самого верхнего уровня окна и все остальные виджеты находятся в нем и так же могут иметь своих детей, пусть будет пока Flex::column(). Нам все равно придется, что то вернуть из функции, а флекс колонка не будет лишней в будущем.

fn ui_builder() -> impl Widget<CountState> {
    Flex::column()
}

А теперь самое интересное. Функция main(). Нам понадобится окно, куда мы передадим для отображения созданный выше виджет, а так же передадим его состояние (в данном случае состояние приложения) и обработаем ошибку, мало ли, подстрахуемся лучше, все равно нам надо что то сделать с Result.

fn main() {
    let main_window = WindowDesc::new(ui_builder());
    AppLauncher::with_window(main_window)
        .launch(CountState { count: 0 })
        .expect("Ошибка запуска приложения.");
}

В итоге, у вас обязано получится:

use druid::widget::{Flex};
use druid::{WindowDesc, Widget, Data, AppLauncher};

#[derive(Clone, Data)]
struct CountState {
    count: i32
}

fn ui_builder() -> impl Widget<CountState> {
    Flex::column()
}

fn main() {
    let main_window = WindowDesc::new(ui_builder());
    AppLauncher::with_window(main_window)
        .launch(CountState { count: 0 })
        .expect("Ошибка запуска приложения.");
}

Запускаем cargo run. Первый раз компилируются все зависимости, повторные компиляции не потребуют много времени, будет перекомпилироваться только наш изменяемый main.rs.

Что же, добавим main_window заголовок и размер окна и заново запустим компиляцию с выполнением, cargo run.

fn main() {
    let main_window = WindowDesc::new(ui_builder())
        .title("Счетчик")
        .window_size((250.0, 90.0));
    AppLauncher::with_window(main_window)
        .launch(CountState { count: 0 })
        .expect("Ошибка запуска приложения.");
}

Совсем другое дело. Теперь пора доработать наш виджет, добавим надпись и будущую информацию значения счетчика в наше пустое окно. Изменим функцию, и импортируем кое что из библиотечных функций, в том числе и виджет label. Ниже измененные участки кода нашего приложения.

use druid::widget::{Flex, Label};
use druid::{WindowDesc, Widget, Data, AppLauncher, Env};

fn ui_builder() -> impl Widget<CountState> {
    let label = Label::new(|data: &CountState, _: &Env| format!("Счетчик: {}", data.count));
    Flex::column().with_child(label)
}

Счетчик есть, нужно позаботится о кнопках инкремента и декремента. Расположим их на следующей строке.

use druid::widget::{Flex, Label, Button};

fn ui_builder() -> impl Widget<CountState> {
    let label = Label::new(|data: &CountState, _: &Env| format!("Счетчик: {}", data.count));
    let increment = Button::new("+");
    let decrement = Button::new("-");
    Flex::column()
        .with_child(label)
        .with_child(Flex::row()
            .with_child(increment)
            .with_child(decrement)
        )
}

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

Остался последний штрих, добавить события для кнопок ”+” и ”-” для изменения счетчика, не зря же мы виджету передали структуру о его состоянии счетчика.

fn ui_builder() -> impl Widget<CountState> {
    let label = Label::new(|data: &CountState, _: &Env| format!("Счетчик: {}", data.count));
    let increment = Button::new("+")
        .on_click(|_ctx, data: &mut CountState, _env| data.count += 1);
    let decrement = Button::new("-")
        .on_click(|_ctx, data: &mut CountState, _env| data.count -= 1);
    Flex::column()
        .with_child(label)
        .with_child(Flex::row()
            .with_child(increment)
            .with_child(decrement)
        )
}

cargo run и вуаля, кнопки изменяют счетчик. Получившийся код даже проще чем в flutter(dart) с его подключением материалов и скафолдами.

Для тех кто ещё не знает rust, или начинает и запутался в изменениях по ходу доработки приложения, полный образец исходного кода привожу ниже.

use druid::widget::{Flex, Label, Button};
use druid::{WindowDesc, Widget, Data, AppLauncher, Env};

#[derive(Clone, Data)]
struct CountState {
    count: i32
}

fn ui_builder() -> impl Widget<CountState> {
    let label = Label::new(|data: &CountState, _: &Env| format!("Счетчик: {}", data.count));
    let increment = Button::new("+")
        .on_click(|_ctx, data: &mut CountState, _env| data.count += 1);
    let decrement = Button::new("-")
        .on_click(|_ctx, data: &mut CountState, _env| data.count -= 1);
    Flex::column()
        .with_child(label)
        .with_child(Flex::row()
            .with_child(increment)
            .with_child(decrement)
        )
}

fn main() {
    let main_window = WindowDesc::new(ui_builder())
        .title("Счетчик")
        .window_size((250.0, 90.0));
    AppLauncher::with_window(main_window)
        .launch(CountState { count: 0 })
        .expect("Ошибка запуска приложения.");
}