From 690c7ea0798dbb1c9470018d79ba5be82cd83a09 Mon Sep 17 00:00:00 2001 From: barsoosayque Date: Sat, 30 Nov 2024 20:13:48 +0700 Subject: [PATCH] editor: reusable property table --- crates/opensi-core/src/components.rs | 5 +- crates/opensi-editor/src/app/package_tab.rs | 63 +++----- crates/opensi-editor/src/app/round_tab.rs | 23 +-- crates/opensi-editor/src/app/theme_tab.rs | 16 +-- crates/opensi-editor/src/element/common.rs | 142 +++++++++---------- crates/opensi-editor/src/element/mod.rs | 2 + crates/opensi-editor/src/element/property.rs | 73 ++++++++++ 7 files changed, 179 insertions(+), 145 deletions(-) create mode 100644 crates/opensi-editor/src/element/property.rs diff --git a/crates/opensi-core/src/components.rs b/crates/opensi-core/src/components.rs index 4638aea..65fbe8a 100644 --- a/crates/opensi-core/src/components.rs +++ b/crates/opensi-core/src/components.rs @@ -26,8 +26,9 @@ pub struct Authors { pub struct Round { #[serde(rename = "@name")] pub name: String, + // TODO: Actual enum of kinds #[serde(rename = "@type", skip_serializing_if = "Option::is_none")] - pub variant: Option, + pub kind: Option, #[serde(rename = "@info", skip_serializing_if = "Option::is_none")] pub info: Option, #[serde(with = "serde_impl::themes")] @@ -45,7 +46,7 @@ pub struct Theme { } impl Theme { - /// Try to guess a price for the next question: + /// Try to guess price for the next question: /// - Either a difference between the last two question prices; /// - Or the last question's price plus 100; /// diff --git a/crates/opensi-editor/src/app/package_tab.rs b/crates/opensi-editor/src/app/package_tab.rs index 53c126f..7cb6224 100644 --- a/crates/opensi-editor/src/app/package_tab.rs +++ b/crates/opensi-editor/src/app/package_tab.rs @@ -1,7 +1,9 @@ use itertools::Itertools; use opensi_core::prelude::*; -use crate::element::{danger_button, simple_row, unselectable_heading, unselectable_label}; +use crate::element::{ + danger_button, string_list, unselectable_heading, unselectable_label, PropertyTable, +}; /// Workarea tab to edit package info. pub fn package_tab(package: &mut Package, selected: &mut Option, ui: &mut egui::Ui) { @@ -35,51 +37,28 @@ pub fn package_tab(package: &mut Package, selected: &mut Option, ui } fn package_info_edit(package: &mut Package, ui: &mut egui::Ui) { - egui_extras::TableBuilder::new(ui) - .id_salt("package-info-edit") - .vscroll(false) - .column(egui_extras::Column::auto()) - .column(egui_extras::Column::remainder()) - .cell_layout(egui::Layout::left_to_right(egui::Align::Min)) - .striped(false) - .body(|mut body| { - simple_row("Название", 20.0, &mut body, |ui| { - ui.text_edit_singleline(&mut package.name); - }); - simple_row("Сложность", 20.0, &mut body, |ui| { - ui.add(egui::DragValue::new(&mut package.difficulty).range(0..=10)); - }); - simple_row("Ограничения", 20.0, &mut body, |ui| { - ui.text_edit_singleline(&mut package.restriction); - }); - simple_row("Дата создания", 20.0, &mut body, |ui| { - ui.text_edit_singleline(&mut package.date); - }); - simple_row("Издатель", 20.0, &mut body, |ui| { - ui.text_edit_singleline(&mut package.publisher); - }); - simple_row("Язык", 20.0, &mut body, |ui| { - ui.text_edit_singleline(&mut package.language); - }); + PropertyTable::new("package-info-properties").show(ui, |mut properties| { + properties.row("Название", |ui| ui.text_edit_singleline(&mut package.name)); + properties.row("Сложность", |ui| { + ui.add(egui::DragValue::new(&mut package.difficulty).range(0..=10)) }); + properties + .row("Ограничения", |ui| ui.text_edit_singleline(&mut package.restriction)); + properties + .row("Дата создания", |ui| ui.text_edit_singleline(&mut package.date)); + properties.row("Издатель", |ui| ui.text_edit_singleline(&mut package.publisher)); + properties.row("Язык", |ui| ui.text_edit_singleline(&mut package.language)); + properties + .multiline_row("Тэги", 2, |ui| string_list("package-tags", &mut package.tags, ui)) + }); } fn package_metadata_edit(package: &Package, ui: &mut egui::Ui) { - egui_extras::TableBuilder::new(ui) - .id_salt("package-metadata-edit") - .vscroll(false) - .column(egui_extras::Column::auto()) - .column(egui_extras::Column::remainder()) - .cell_layout(egui::Layout::left_to_right(egui::Align::Min)) - .striped(true) - .body(|mut body| { - simple_row("ID пакета", 20.0, &mut body, |ui| { - ui.label(&package.id); - }); - simple_row("Версия пакета", 20.0, &mut body, |ui| { - ui.label(format!("{:.1}", package.version)); - }); - }); + PropertyTable::new("package-metadata-properties").readonly(true).show(ui, |mut properties| { + properties.row("ID пакета", |ui| ui.label(&package.id)); + properties + .row("Версия пакета", |ui| ui.label(format!("{:.1}", package.version))); + }); } fn package_rounds(package: &mut Package, selected: &mut Option, ui: &mut egui::Ui) { diff --git a/crates/opensi-editor/src/app/round_tab.rs b/crates/opensi-editor/src/app/round_tab.rs index 9a66d3e..b89a49a 100644 --- a/crates/opensi-editor/src/app/round_tab.rs +++ b/crates/opensi-editor/src/app/round_tab.rs @@ -1,8 +1,6 @@ use opensi_core::prelude::*; -use crate::element::{ - error_label, info_edit, simple_row, unselectable_heading, unselectable_label, Card, -}; +use crate::element::{error_label, info_edit, unselectable_heading, Card, PropertyTable}; /// Workarea tab to edit round info and its themes. pub fn round_tab( @@ -47,21 +45,12 @@ pub fn round_tab( } fn round_edit(round: &mut Round, ui: &mut egui::Ui) { - egui_extras::TableBuilder::new(ui) - .id_salt("round-edit") - .column(egui_extras::Column::auto()) - .column(egui_extras::Column::remainder()) - .cell_layout(egui::Layout::left_to_right(egui::Align::Min)) - .striped(false) - .body(|mut body| { - simple_row("Название", 20.0, &mut body, |ui| { - ui.text_edit_singleline(&mut round.name); - }); - simple_row("Вариант", 20.0, &mut body, |ui| { - // TODO: variant enum - unselectable_label(format!("TODO: {:?}", round.variant), ui); - }); + PropertyTable::new("round-properties").show(ui, |mut properties| { + properties.row("Название", |ui| ui.text_edit_singleline(&mut round.name)); + properties.row("Тип", |ui| { + ui.add_enabled_ui(false, |ui| ui.label(format!("{:?}?", round.kind))).inner }); + }); } fn round_themes( diff --git a/crates/opensi-editor/src/app/theme_tab.rs b/crates/opensi-editor/src/app/theme_tab.rs index e969310..593bb46 100644 --- a/crates/opensi-editor/src/app/theme_tab.rs +++ b/crates/opensi-editor/src/app/theme_tab.rs @@ -1,6 +1,6 @@ use opensi_core::prelude::*; -use crate::element::{error_label, info_edit, simple_row, unselectable_heading, Card}; +use crate::element::{error_label, info_edit, unselectable_heading, Card, PropertyTable}; pub fn theme_tab( package: &mut Package, @@ -44,17 +44,9 @@ pub fn theme_tab( } fn theme_edit(theme: &mut Theme, ui: &mut egui::Ui) { - egui_extras::TableBuilder::new(ui) - .id_salt("theme-edit") - .column(egui_extras::Column::auto()) - .column(egui_extras::Column::remainder()) - .cell_layout(egui::Layout::left_to_right(egui::Align::Min)) - .striped(false) - .body(|mut body| { - simple_row("Название", 20.0, &mut body, |ui| { - ui.text_edit_singleline(&mut theme.name); - }); - }); + PropertyTable::new("theme-properties").show(ui, |mut properties| { + properties.row("Название", |ui| ui.text_edit_singleline(&mut theme.name)); + }); } fn theme_questions( diff --git a/crates/opensi-editor/src/element/common.rs b/crates/opensi-editor/src/element/common.rs index d91847b..f16a9cd 100644 --- a/crates/opensi-editor/src/element/common.rs +++ b/crates/opensi-editor/src/element/common.rs @@ -2,6 +2,8 @@ use std::{borrow::Cow, fmt::Display}; use opensi_core::prelude::*; +use super::PropertyTable; + /// A generic error label. pub fn error_label(error: impl Display, ui: &mut egui::Ui) { let text = @@ -60,67 +62,73 @@ pub fn unselectable_label(text: impl Into, ui: &mut egui::Ui) ui.add(egui::Label::new(text).selectable(false)) } -pub fn string_list(id: impl Into, list: &mut Vec, ui: &mut egui::Ui) { +pub fn string_list( + id: impl Into, + list: &mut Vec, + ui: &mut egui::Ui, +) -> egui::Response { ui.push_id(id.into(), |ui| { - ui.vertical(|ui| { - if list.is_empty() { - unselectable_label("Пусто...", ui); - } else { - ui.horizontal(|ui| { - let mut deleted_index = None; - - for (index, item) in list.iter().enumerate() { - egui::Frame::none() - .rounding(4.0) - .inner_margin(egui::Margin { left: 4.0, ..Default::default() }) - .fill(ui.style().visuals.widgets.inactive.bg_fill) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label(item); - if ui.small_button("❌").clicked() { - deleted_index = Some(index); - } + ui.with_layout( + egui::Layout::top_down(egui::Align::Min) + .with_cross_justify(true) + .with_main_align(egui::Align::Center), + |ui| { + if list.is_empty() { + unselectable_label("Пусто...", ui); + } else { + ui.horizontal(|ui| { + let mut deleted_index = None; + + for (index, item) in list.iter().enumerate() { + egui::Frame::none() + .rounding(4.0) + .inner_margin(egui::Margin { left: 4.0, ..Default::default() }) + .fill(ui.style().visuals.widgets.inactive.bg_fill) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(item); + if ui.small_button("❌").clicked() { + deleted_index = Some(index); + } + }); }); - }); - } + } - if let Some(index) = deleted_index { - list.remove(index); - } - }); - } + if let Some(index) = deleted_index { + list.remove(index); + } + }); + } + + ui.spacing(); - ui.horizontal(|ui| { let new_item_id = ui.id().with("new"); let mut text = ui.memory_mut(|memory| { memory.data.get_temp_mut_or_default::(new_item_id).clone() }); - - if ui.button("➕").clicked() && !text.is_empty() { - list.push(text.clone()); - ui.memory_mut(|memory| memory.data.remove_temp::(new_item_id)); - } - - if ui.text_edit_singleline(&mut text).changed() { - ui.memory_mut(|memory| memory.data.insert_temp(new_item_id, text)); - } - }); - }); - }); -} - -pub fn simple_row( - label: impl AsRef, - height: f32, - body: &mut egui_extras::TableBody, - content: impl FnOnce(&mut egui::Ui), -) { - body.row(height, |mut row| { - row.col(|ui| { - ui.label(label.as_ref()); - }); - row.col(content); - }); + egui_extras::StripBuilder::new(ui) + .size(egui_extras::Size::exact(22.0)) + .size(egui_extras::Size::remainder()) + .horizontal(|mut strip| { + strip.cell(|ui| { + if ui.button("➕").clicked() && !text.is_empty() { + list.push(text.clone()); + ui.memory_mut(|memory| { + memory.data.remove_temp::(new_item_id) + }); + } + }); + + strip.cell(|ui| { + if ui.text_edit_singleline(&mut text).changed() { + ui.memory_mut(|memory| memory.data.insert_temp(new_item_id, text)); + } + }); + }); + }, + ); + }) + .response } pub fn info_edit(info: &mut Option, ui: &mut egui::Ui) { @@ -131,24 +139,14 @@ pub fn info_edit(info: &mut Option, ui: &mut egui::Ui) { return; }; - egui_extras::TableBuilder::new(ui) - .id_salt("round-info-edit") - .column(egui_extras::Column::auto()) - .column(egui_extras::Column::remainder()) - .cell_layout(egui::Layout::left_to_right(egui::Align::Min)) - .striped(true) - .body(|mut body| { - simple_row("Авторы", 40.0, &mut body, |ui| { - string_list("round-authors", &mut info.authors, ui); - }); - simple_row("Источники", 40.0, &mut body, |ui| { - string_list("round-sources", &mut info.sources, ui); - }); - simple_row("Комментарий", 20.0, &mut body, |ui| { - ui.text_edit_singleline(&mut info.comments); - }); - simple_row("Расширения", 20.0, &mut body, |ui| { - ui.text_edit_singleline(&mut info.extension); - }); + PropertyTable::new("info-properties").show(ui, |mut properties| { + properties.multiline_row("Авторы", 2, |ui| { + string_list("info-properties-authors", &mut info.authors, ui) }); + properties.multiline_row("Источники", 2, |ui| { + string_list("info-properties-sources", &mut info.sources, ui) + }); + properties.row("Комментарий", |ui| ui.text_edit_singleline(&mut info.comments)); + properties.row("Расширения", |ui| ui.text_edit_singleline(&mut info.extension)); + }); } diff --git a/crates/opensi-editor/src/element/mod.rs b/crates/opensi-editor/src/element/mod.rs index 1d9a7f2..f4cd2d0 100644 --- a/crates/opensi-editor/src/element/mod.rs +++ b/crates/opensi-editor/src/element/mod.rs @@ -1,5 +1,7 @@ pub mod card; pub mod common; +pub mod property; pub use card::Card; pub use common::*; +pub use property::PropertyTable; diff --git a/crates/opensi-editor/src/element/property.rs b/crates/opensi-editor/src/element/property.rs new file mode 100644 index 0000000..4e8f5f7 --- /dev/null +++ b/crates/opensi-editor/src/element/property.rs @@ -0,0 +1,73 @@ +/// Row builder for [`PropertyTable`]. +pub struct Properties<'t> { + row_height: f32, + readonly: bool, + body: egui_extras::TableBody<'t>, +} + +impl<'t> Properties<'t> { + /// Emplace a new row with named property. + pub fn row( + &mut self, + name: &'static str, + content: impl FnMut(&mut egui::Ui) -> egui::Response, + ) { + self.multiline_row(name, 1, content); + } + + /// Emplace a new row with named property which takes a few lines. + pub fn multiline_row( + &mut self, + name: &'static str, + lines: usize, + mut content: impl FnMut(&mut egui::Ui) -> egui::Response, + ) { + self.body.row(self.row_height * lines as f32, |mut row| { + row.col(|ui| { + ui.add(egui::Label::new(name).selectable(false)); + }); + row.col(|ui| { + ui.add_enabled_ui(!self.readonly, |ui| { + content(ui); + }); + }); + }); + } +} + +/// Table with two columns: property names, and property values. +pub struct PropertyTable { + id: egui::Id, + row_height: f32, + readonly: bool, +} + +impl PropertyTable { + pub fn new(id: impl std::hash::Hash) -> Self { + let id = egui::Id::new(id); + Self { id, row_height: 22.0, readonly: false } + } + + pub fn readonly(self, value: bool) -> Self { + Self { readonly: value, ..self } + } + + pub fn show(self, ui: &mut egui::Ui, mut builder: impl FnMut(Properties)) { + egui_extras::TableBuilder::new(ui) + .id_salt(self.id) + .column(egui_extras::Column::auto()) + .column(egui_extras::Column::remainder()) + .cell_layout( + egui::Layout::left_to_right(egui::Align::Center) + .with_main_justify(true) + .with_main_align(egui::Align::Min), + ) + .striped(true) + .vscroll(false) + .body(|body| { + let properties = + Properties { row_height: self.row_height, readonly: self.readonly, body }; + builder(properties); + }); + } +}