mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2025-02-25 18:55:26 -06:00
feat: use cambalache and composite templates for the ui (currently for the vulkan info window only)
feat: use blueprint
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
23
lact-gui/ui/vulkan_feature_row.blp
Normal file
23
lact-gui/ui/vulkan_feature_row.blp
Normal 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';
|
||||
}
|
||||
}
|
||||
35
lact-gui/ui/vulkan_features_window.blp
Normal file
35
lact-gui/ui/vulkan_features_window.blp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user