Add log view UI with transaction entry form

Expense/Income toggle, amount entry, currency dropdown, category
dropdown (filtered by transaction type), date picker with calendar
popover, optional note field, save button, and recent transactions
placeholder section.
This commit is contained in:
2026-03-02 00:05:10 +02:00
parent 6daec1ea38
commit bc4a3453f8
3 changed files with 219 additions and 1 deletions

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

@@ -0,0 +1,204 @@
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();
// Amount
let amount_row = adw::EntryRow::builder()
.title("Amount")
.build();
amount_row.set_input_purpose(gtk::InputPurpose::Number);
form_group.add(&amount_row);
// Currency
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);
// Category
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);
// Date picker
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);
// Note
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);
}
}
}
}