Add log view UI with transaction entry form

This commit is contained in:
2026-03-02 00:05:10 +02:00
parent 7af27c06c1
commit 657ea5fe76
3 changed files with 214 additions and 1 deletions

199
outlay-gtk/src/log_view.rs Normal file
View File

@@ -0,0 +1,199 @@
use adw::prelude::*;
use gtk::glib;
pub struct LogView {
pub container: gtk::Box,
pub type_toggle: gtk::ToggleButton,
pub amount_row: adw::EntryRow,
pub currency_row: adw::ComboRow,
pub category_row: adw::ComboRow,
pub date_label: gtk::Label,
pub note_row: adw::EntryRow,
pub save_button: gtk::Button,
pub recent_group: adw::PreferencesGroup,
}
impl LogView {
pub fn new() -> Self {
let container = gtk::Box::new(gtk::Orientation::Vertical, 0);
let clamp = adw::Clamp::new();
clamp.set_maximum_size(600);
clamp.set_margin_top(24);
clamp.set_margin_bottom(24);
clamp.set_margin_start(12);
clamp.set_margin_end(12);
let inner = gtk::Box::new(gtk::Orientation::Vertical, 24);
// -- Transaction type toggle --
let type_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
type_box.add_css_class("linked");
type_box.set_halign(gtk::Align::Center);
let expense_btn = gtk::ToggleButton::with_label("Expense");
expense_btn.set_active(true);
expense_btn.set_hexpand(true);
let income_btn = gtk::ToggleButton::with_label("Income");
income_btn.set_group(Some(&expense_btn));
income_btn.set_hexpand(true);
type_box.append(&expense_btn);
type_box.append(&income_btn);
// -- Form group --
let form_group = adw::PreferencesGroup::builder()
.title("New Transaction")
.build();
let amount_row = adw::EntryRow::builder()
.title("Amount")
.build();
amount_row.set_input_purpose(gtk::InputPurpose::Number);
form_group.add(&amount_row);
let currency_model = gtk::StringList::new(&[
"USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "CNY", "INR", "BRL",
]);
let currency_row = adw::ComboRow::builder()
.title("Currency")
.model(&currency_model)
.build();
form_group.add(&currency_row);
let category_model = gtk::StringList::new(&[]);
let category_row = adw::ComboRow::builder()
.title("Category")
.model(&category_model)
.build();
// Populate with default expense categories
Self::populate_categories(&category_model, true);
form_group.add(&category_row);
let today = glib::DateTime::now_local().unwrap();
let date_str = today.format("%Y-%m-%d").unwrap().to_string();
let date_label = gtk::Label::new(Some(&date_str));
date_label.set_halign(gtk::Align::End);
date_label.set_hexpand(true);
let calendar = gtk::Calendar::new();
let popover = gtk::Popover::new();
popover.set_child(Some(&calendar));
let date_menu_btn = gtk::MenuButton::new();
date_menu_btn.set_popover(Some(&popover));
date_menu_btn.set_icon_name("x-office-calendar-symbolic");
date_menu_btn.add_css_class("flat");
let date_box = gtk::Box::new(gtk::Orientation::Horizontal, 8);
date_box.append(&date_label);
date_box.append(&date_menu_btn);
let date_row = adw::ActionRow::builder()
.title("Date")
.build();
date_row.add_suffix(&date_box);
date_row.set_activatable_widget(Some(&date_menu_btn));
// Connect calendar day-selected to update the label
let date_label_ref = date_label.clone();
let popover_ref = popover.clone();
calendar.connect_day_selected(move |cal| {
let dt = cal.date();
let formatted = dt.format("%Y-%m-%d").unwrap().to_string();
date_label_ref.set_label(&formatted);
popover_ref.popdown();
});
form_group.add(&date_row);
let note_row = adw::EntryRow::builder()
.title("Note (optional)")
.build();
form_group.add(&note_row);
// -- Save button --
let save_button = gtk::Button::with_label("Save");
save_button.add_css_class("suggested-action");
save_button.add_css_class("pill");
save_button.set_halign(gtk::Align::Center);
save_button.set_margin_top(8);
// -- Wire type toggle to filter categories --
let category_model_ref = category_model.clone();
expense_btn.connect_toggled(move |btn| {
if btn.is_active() {
Self::populate_categories(&category_model_ref, true);
}
});
let category_model_ref2 = category_model.clone();
income_btn.connect_toggled(move |btn| {
if btn.is_active() {
Self::populate_categories(&category_model_ref2, false);
}
});
// -- Recent transactions (placeholder) --
let recent_group = adw::PreferencesGroup::builder()
.title("Recent")
.build();
let placeholder = adw::ActionRow::builder()
.title("No transactions yet")
.build();
placeholder.add_css_class("dim-label");
recent_group.add(&placeholder);
// -- Assemble --
inner.append(&type_box);
inner.append(&form_group);
inner.append(&save_button);
inner.append(&recent_group);
clamp.set_child(Some(&inner));
container.append(&clamp);
LogView {
container,
type_toggle: expense_btn,
amount_row,
currency_row,
category_row,
date_label,
note_row,
save_button,
recent_group,
}
}
fn populate_categories(model: &gtk::StringList, is_expense: bool) {
// Remove existing items
while model.n_items() > 0 {
model.remove(0);
}
if is_expense {
let cats = [
"Food & Dining", "Groceries", "Transport", "Housing",
"Utilities", "Healthcare", "Entertainment", "Shopping",
"Education", "Personal Care", "Travel", "Insurance",
"Gifts & Donations", "Other Expense",
];
for c in cats {
model.append(c);
}
} else {
let cats = [
"Salary", "Freelance", "Investments", "Gifts Received",
"Refunds", "Other Income",
];
for c in cats {
model.append(c);
}
}
}
}

View File

@@ -1,3 +1,4 @@
mod log_view;
mod window;
use adw::prelude::*;

View File

@@ -1,9 +1,12 @@
use adw::prelude::*;
use crate::log_view::LogView;
pub struct MainWindow {
pub window: adw::ApplicationWindow,
pub split_view: adw::NavigationSplitView,
pub content_stack: gtk::Stack,
pub log_view: LogView,
}
struct SidebarItem {
@@ -26,7 +29,16 @@ impl MainWindow {
let content_stack = gtk::Stack::new();
content_stack.set_transition_type(gtk::StackTransitionType::Crossfade);
for item in SIDEBAR_ITEMS {
// Log view - real widget
let log_view = LogView::new();
let log_scroll = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never)
.child(&log_view.container)
.build();
content_stack.add_named(&log_scroll, Some("log"));
// Remaining pages are placeholders for now
for item in &SIDEBAR_ITEMS[1..] {
let page = adw::StatusPage::builder()
.title(item.label)
.icon_name(item.icon)
@@ -98,6 +110,7 @@ impl MainWindow {
window,
split_view,
content_stack,
log_view,
}
}