В текущее время растет популярность 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("Ошибка запуска приложения.");
}