feat: use cambalache and composite templates for the ui (currently for the vulkan info window only)

feat: use blueprint
This commit is contained in:
Ilya Zlobintsev
2023-11-05 09:02:05 +02:00
parent 51e187ef2a
commit 92ba99fc16
11 changed files with 361 additions and 160 deletions

View File

@@ -11,10 +11,13 @@ gtk-tests = []
[dependencies]
lact-client = { path = "../lact-client" }
lact-daemon = { path = "../lact-daemon" }
gtk = { version = "0.7", package = "gtk4", features = ["v4_6"] }
gtk = { version = "0.7", package = "gtk4", features = ["v4_6", "blueprint"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
anyhow = "1.0"
[build-dependencies]
glib-build-tools = "0.18.0"
[dev-dependencies]
pretty_assertions = "1.4.0"

View File

@@ -1,25 +0,0 @@
use gio::subclass::prelude::*;
use gtk::{
gio,
glib::{self, Properties},
prelude::*,
};
use std::cell::{Cell, RefCell};
#[derive(Debug, Default, Properties)]
#[properties(wrapper_type = super::FeatureModel)]
pub struct FeatureModel {
#[property(set, get)]
pub name: RefCell<String>,
#[property(set, get)]
pub supported: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for FeatureModel {
const NAME: &'static str = "VulkanFeatureModel";
type Type = super::FeatureModel;
}
#[glib::derived_properties]
impl ObjectImpl for FeatureModel {}

View File

@@ -1,16 +0,0 @@
mod imp;
use gtk::glib::{self, Object};
glib::wrapper! {
pub struct FeatureModel(ObjectSubclass<imp::FeatureModel>);
}
impl FeatureModel {
pub fn new(name: String, supported: bool) -> Self {
Object::builder()
.property("name", name)
.property("supported", supported)
.build()
}
}

View File

@@ -0,0 +1,48 @@
use gtk::glib::{self, Object};
glib::wrapper! {
pub struct VulkanFeature(ObjectSubclass<imp::VulkanFeature>);
}
impl VulkanFeature {
pub fn new(name: String, supported: bool) -> Self {
Object::builder()
.property("name", name)
.property("supported", supported)
.build()
}
}
impl Default for VulkanFeature {
fn default() -> Self {
Self::new(String::new(), false)
}
}
mod imp {
use gio::subclass::prelude::*;
use gtk::{
gio,
glib::{self, Properties},
prelude::*,
};
use std::cell::{Cell, RefCell};
#[derive(Debug, Default, Properties)]
#[properties(wrapper_type = super::VulkanFeature)]
pub struct VulkanFeature {
#[property(set, get)]
pub name: RefCell<String>,
#[property(set, get)]
pub supported: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for VulkanFeature {
const NAME: &'static str = "VulkanFeature";
type Type = super::VulkanFeature;
}
#[glib::derived_properties]
impl ObjectImpl for VulkanFeature {}
}

View File

@@ -0,0 +1,132 @@
pub mod feature;
mod row;
use glib::Object;
use gtk::{gio, glib};
glib::wrapper! {
pub struct VulkanFeaturesWindow(ObjectSubclass<imp::VulkanFeaturesWindow>)
@extends gtk::Box, gtk::Widget, gtk::Window,
@implements gtk::Orientable, gtk::Accessible, gtk::Buildable;
}
impl VulkanFeaturesWindow {
pub fn new(title: &str, model: gio::ListModel) -> Self {
Object::builder()
.property("title", title)
.property("model", model)
.build()
}
}
mod imp {
use super::{feature::VulkanFeature, row::VulkanFeatureRow};
use glib::Properties;
use gtk::{
gio,
glib::{self, clone, subclass::InitializingObject},
prelude::*,
subclass::{
prelude::*,
widget::{CompositeTemplateClass, WidgetImpl},
},
CompositeTemplate, Expression, FilterListModel, PropertyExpression, SearchEntry,
SignalListItemFactory, StringFilter, TemplateChild,
};
use std::cell::RefCell;
#[derive(CompositeTemplate, Properties, Default)]
#[properties(wrapper_type = super::VulkanFeaturesWindow)]
#[template(file = "ui/vulkan_features_window.blp")]
pub struct VulkanFeaturesWindow {
#[property(get, set)]
model: RefCell<Option<gio::ListModel>>,
#[template_child]
features_factory: TemplateChild<SignalListItemFactory>,
#[template_child]
filter_model: TemplateChild<FilterListModel>,
#[template_child]
search_filter: TemplateChild<StringFilter>,
#[template_child]
search_entry: TemplateChild<SearchEntry>,
}
#[glib::object_subclass]
impl ObjectSubclass for VulkanFeaturesWindow {
const NAME: &'static str = "VulkanFeaturesWindow";
type Type = super::VulkanFeaturesWindow;
type ParentType = gtk::Window;
fn class_init(class: &mut Self::Class) {
class.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// impl Default for VulkanFeaturesWindow {
// fn default() -> Self {
// Self {
// model: RefCell::new(gio::ListStore::new::<VulkanFeature>().into()),
// features_factory: Default::default(),
// filter_model: Default::default(),
// search_filter: Default::default(),
// search_entry: Default::default(),
// }
// }
// }
#[glib::derived_properties]
impl ObjectImpl for VulkanFeaturesWindow {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.bind_property("model", &self.filter_model.get(), "model")
.sync_create()
.build();
let expression =
PropertyExpression::new(VulkanFeature::static_type(), Expression::NONE, "name");
self.search_filter.set_expression(Some(&expression));
self.search_entry.connect_search_changed(
clone!(@strong self.search_filter as filter => move |entry| {
if entry.text().is_empty() {
filter.set_search(None);
} else {
filter.set_search(Some(entry.text().as_str()));
}
}),
);
self.features_factory.connect_setup(|_, list_item| {
let feature = VulkanFeature::default();
let row = VulkanFeatureRow::new(feature);
list_item.set_child(Some(&row));
});
self.features_factory.connect_bind(|_, list_item| {
let feature = list_item
.item()
.and_downcast::<VulkanFeature>()
.expect("The item has to be a VulkanFeature");
let row = list_item
.child()
.and_downcast::<VulkanFeatureRow>()
.expect("The child has to be a VulkanFeatureRow");
row.set_feature(feature);
});
}
}
impl WidgetImpl for VulkanFeaturesWindow {}
impl WindowImpl for VulkanFeaturesWindow {}
impl ApplicationWindowImpl for VulkanFeaturesWindow {}
}

View File

@@ -0,0 +1,85 @@
use super::feature::VulkanFeature;
use glib::Object;
use gtk::glib;
glib::wrapper! {
pub struct VulkanFeatureRow(ObjectSubclass<imp::VulkanFeatureRow>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Orientable, gtk::Accessible, gtk::Buildable;
}
impl VulkanFeatureRow {
pub fn new(feature: VulkanFeature) -> Self {
Object::builder().property("feature", feature).build()
}
}
mod imp {
use crate::app::root_stack::info_page::vulkan_info::feature_window::feature::VulkanFeature;
use glib::Properties;
use gtk::{
glib::{self, subclass::InitializingObject},
prelude::*,
subclass::{
prelude::*,
widget::{CompositeTemplateClass, WidgetImpl},
},
CompositeTemplate, Image, Label, TemplateChild,
};
use std::cell::RefCell;
#[derive(CompositeTemplate, Default, Properties)]
#[properties(wrapper_type = super::VulkanFeatureRow)]
#[template(file = "ui/vulkan_feature_row.blp")]
pub struct VulkanFeatureRow {
#[template_child]
name_label: TemplateChild<Label>,
#[template_child]
available_image: TemplateChild<Image>,
#[property(get, set)]
feature: RefCell<VulkanFeature>,
}
#[glib::object_subclass]
impl ObjectSubclass for VulkanFeatureRow {
const NAME: &'static str = "VulkanFeatureRow";
type Type = super::VulkanFeatureRow;
type ParentType = gtk::Box;
fn class_init(class: &mut Self::Class) {
class.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for VulkanFeatureRow {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.bind_property("feature", &self.name_label.get(), "label")
.transform_to(|_, feature: VulkanFeature| Some(feature.name()))
.sync_create()
.build();
obj.bind_property("feature", &self.available_image.get(), "icon-name")
.transform_to(|_, feature: VulkanFeature| {
if feature.supported() {
Some("emblem-ok-symbolic")
} else {
Some("action-unavailable-symbolic")
}
})
.sync_create()
.build();
}
}
impl WidgetImpl for VulkanFeatureRow {}
impl BoxImpl for VulkanFeatureRow {}
}

View File

@@ -1,15 +1,14 @@
mod feature_model;
mod feature_window;
use crate::app::root_stack::{label_row, values_row};
use self::feature_window::VulkanFeaturesWindow;
use self::feature_model::FeatureModel;
use super::values_grid;
use crate::app::root_stack::info_page::vulkan_info::feature_window::feature::VulkanFeature;
use crate::app::root_stack::{label_row, values_row};
use glib::clone;
use gtk::prelude::*;
use gtk::*;
use lact_client::schema::VulkanInfo;
use std::cell::RefCell;
use std::rc::Rc;
use tracing::trace;
#[derive(Clone)]
@@ -19,16 +18,16 @@ pub struct VulkanInfoFrame {
version_label: Label,
driver_name_label: Label,
driver_version_label: Label,
features: Rc<RefCell<Vec<FeatureModel>>>,
extensions: Rc<RefCell<Vec<FeatureModel>>>,
features_model: gio::ListStore,
extensions_model: gio::ListStore,
}
impl VulkanInfoFrame {
pub fn new() -> Self {
let container = Box::new(Orientation::Vertical, 0);
let features = Rc::new(RefCell::new(Vec::new()));
let extensions = Rc::new(RefCell::new(Vec::new()));
let features_model = gio::ListStore::new::<VulkanFeature>();
let extensions_model = gio::ListStore::new::<VulkanFeature>();
let grid = values_grid();
@@ -38,14 +37,14 @@ impl VulkanInfoFrame {
let driver_version_label = label_row("Driver version:", &grid, 3, 0, true);
let show_features_button = Button::builder().label("Show").halign(Align::End).build();
show_features_button.connect_clicked(clone!(@strong features => move |_| {
show_list_window("Vulkan features", &features.borrow());
show_features_button.connect_clicked(clone!(@strong features_model => move |_| {
show_features_window("Vulkan features", features_model.clone());
}));
values_row("Features:", &grid, &show_features_button, 4, 0);
let show_extensions_button = Button::builder().label("Show").halign(Align::End).build();
show_extensions_button.connect_clicked(clone!(@strong extensions => move |_| {
show_list_window("Vulkan extensions", &extensions.borrow());
show_extensions_button.connect_clicked(clone!(@strong extensions_model => move |_| {
show_features_window("Vulkan extensions", extensions_model.clone());
}));
values_row("Extensions:", &grid, &show_extensions_button, 5, 0);
@@ -57,8 +56,8 @@ impl VulkanInfoFrame {
version_label,
driver_name_label,
driver_version_label,
features,
extensions,
features_model,
extensions_model,
}
}
@@ -80,111 +79,21 @@ impl VulkanInfoFrame {
vulkan_info.driver.info.as_deref().unwrap_or_default(),
));
let features_vec: Vec<_> = vulkan_info
.features
.iter()
.map(|(name, supported)| FeatureModel::new(name.to_string(), *supported))
.collect();
self.features.replace(features_vec);
self.features_model.remove_all();
for (name, supported) in &vulkan_info.features {
let feature = VulkanFeature::new(name.to_string(), *supported);
self.features_model.append(&feature);
}
let extensions_vec: Vec<_> = vulkan_info
.extensions
.iter()
.map(|(name, supported)| FeatureModel::new(name.to_string(), *supported))
.collect();
self.extensions.replace(extensions_vec);
self.extensions_model.remove_all();
for (name, supported) in &vulkan_info.extensions {
let extension = VulkanFeature::new(name.to_string(), *supported);
self.extensions_model.append(&extension);
}
}
}
fn show_list_window(title: &str, items: &[FeatureModel]) {
let window = Window::builder()
.title(title)
.width_request(500)
.height_request(700)
.resizable(false)
.build();
let base_model = gio::ListStore::new::<FeatureModel>();
base_model.extend_from_slice(items);
let expression = PropertyExpression::new(FeatureModel::static_type(), Expression::NONE, "name");
let filter = StringFilter::builder()
.match_mode(StringFilterMatchMode::Substring)
.ignore_case(true)
.expression(expression)
.build();
let entry = SearchEntry::builder().hexpand(true).build();
entry.connect_search_changed(clone!(@weak filter => move |entry| {
if entry.text().is_empty() {
filter.set_search(None);
} else {
filter.set_search(Some(entry.text().as_str()));
}
}));
let filter_model = FilterListModel::builder()
.model(&base_model)
.filter(&filter)
.incremental(true)
.build();
let selection_model = NoSelection::new(Some(filter_model));
let factory = gtk::SignalListItemFactory::new();
factory.connect_setup(move |_factory, item| {
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
let label = Label::builder()
.margin_top(5)
.margin_bottom(5)
.selectable(true)
.hexpand(true)
.halign(Align::Start)
.build();
let image = Image::new();
let vbox = Box::builder()
.orientation(Orientation::Horizontal)
.spacing(5)
.margin_start(10)
.margin_end(10)
.build();
vbox.append(&label);
vbox.append(&image);
item.set_child(Some(&vbox));
});
factory.connect_bind(move |_factory, item| {
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
let model = item.item().and_downcast::<FeatureModel>().unwrap();
let vbox = item.child().and_downcast::<Box>().unwrap();
let children = vbox.observe_children();
let label = children.item(0).and_downcast::<Label>().unwrap();
let image = children.item(1).and_downcast::<Image>().unwrap();
let text = model.property::<String>("name");
let supported = model.property::<bool>("supported");
label.set_text(&text);
let icon_name = if supported {
"emblem-ok-symbolic"
} else {
"action-unavailable-symbolic"
};
image.set_icon_name(Some(icon_name));
});
let list_view = ListView::new(Some(selection_model), Some(factory));
let scroll_window = ScrolledWindow::builder()
.child(&list_view)
.vexpand(true)
.build();
let vbox = Box::new(Orientation::Vertical, 5);
vbox.append(&entry);
vbox.append(&scroll_window);
window.set_child(Some(&vbox));
fn show_features_window(title: &str, model: gio::ListStore) {
let window = VulkanFeaturesWindow::new(title, model.into());
window.present();
}

View File

@@ -8,7 +8,7 @@ use tracing::{error, info, metadata::LevelFilter};
use tracing_subscriber::EnvFilter;
const GUI_VERSION: &str = env!("CARGO_PKG_VERSION");
const APP_ID: &str = "io.github.lact-linux";
const APP_ID: &str = "io.github.lact-linux1";
pub fn run(args: GuiArgs) -> anyhow::Result<()> {
let env_filter = EnvFilter::builder()

View File

@@ -0,0 +1,23 @@
using Gtk 4.0;
template $VulkanFeatureRow: Box {
focus-on-click: false;
hexpand: true;
hexpand-set: true;
margin-bottom: 10;
margin-end: 10;
margin-start: 10;
margin-top: 10;
Label name_label {
halign: start;
hexpand: true;
label: 'feature name';
selectable: true;
}
Image available_image {
halign: end;
icon-name: 'action-unavailable-symbolic';
}
}

View File

@@ -0,0 +1,35 @@
using Gtk 4.0;
template $VulkanFeaturesWindow: Window {
default-height: 700;
default-width: 500;
Box {
orientation: vertical;
SearchEntry search_entry {}
ScrolledWindow {
vexpand: true;
ListView {
factory: features_factory;
model: selection_model;
show-separators: true;
}
}
}
}
SignalListItemFactory features_factory {}
NoSelection selection_model {
model: filter_model;
}
StringFilter search_filter {}
FilterListModel filter_model {
filter: search_filter;
incremental: true;
}