Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
"masonry_winit",

"xilem_web",
"xilem_web/web_examples/components",
"xilem_web/web_examples/counter",
"xilem_web/web_examples/counter_custom_element",
"xilem_web/web_examples/elm",
Expand Down
22 changes: 22 additions & 0 deletions xilem_web/web_examples/components/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "components"
license.workspace = true
edition.workspace = true
rust-version.workspace = true

[[bin]]
name = "components"
test = false
doctest = false
doc = false
bench = false

[lints]
workspace = true

[dependencies]
console_error_panic_hook = "0.1.7"
time = { workspace = true, features = ["formatting", "local-offset", "macros", "wasm-bindgen"] }
wasm-bindgen = "0.2.108"
web-sys = "0.3.85"
xilem_web = { path = "../.." }
8 changes: 8 additions & 0 deletions xilem_web/web_examples/components/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>Components | Xilem Web</title>
<link data-trunk rel="css" href="style.css" />
</head>
<body></body>
</html>
44 changes: 44 additions & 0 deletions xilem_web/web_examples/components/src/card.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2026 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0

use xilem_web::{DomFragment, core::map_action, elements::html, interfaces::Element};

// The `CardAction` is a composition of
// the variations of the card component
// (only `Toggle` here so far)
// and the generic actions `A` of the child.
pub(crate) enum Action<A> {
Toggle,
Child(A),
}

impl<A> xilem_web::Action for Action<A> {}

// This view is generic about its child and its actions.
// It also has no state of its own,
// but only communicates with the parents via the [`CardAction`].
pub(crate) fn view<State, Child, ChildAction>(
title: &'static str,
collapsed: bool,
content: Child,
) -> impl Element<State, Action<ChildAction>>
where
Child: DomFragment<State, ChildAction>,
State: 'static,
ChildAction: 'static,
{
let content = map_action(
html::div(content)
.class("content")
.class(collapsed.then_some("hidden")),
|_, msg| Action::Child(msg),
);

html::div((
html::h3(title)
.class("title")
.on_click(|_, _| Action::Toggle),
content,
))
.class("card")
}
166 changes: 166 additions & 0 deletions xilem_web/web_examples/components/src/date_picker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2026 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0

use time::{Date, Duration, OffsetDateTime};
use xilem_web::{elements::html, interfaces::Element, modifiers::style};

#[derive(Debug, Default)]
pub(crate) struct State {
pub selected: Option<Date>,
pub popup_open: bool,
pub shown_month: Option<Date>,
}

pub(crate) enum Action {
DateChanged(Option<Date>),
Cancelled,
}

impl xilem_web::Action for Action {}

// /// Renders only the calendar popup (always visible, no input field).
// /// Intended for use in overlays where the caller controls positioning and backdrop.
pub(crate) fn view(state: &State) -> impl Element<State, Action> + use<> {
let clear_button = html::button("Clear").on_click(|state: &mut State, _| {
state.selected = None;
state.shown_month = None;
Action::DateChanged(None)
});

let today_button = html::button("Today").on_click(|state: &mut State, _| {
let today = today();
state.selected = Some(today);
state.shown_month = Some(today);
Action::DateChanged(Some(today))
});

let cancel_button = html::button("\u{2716}")
.class("ml-auto")
.on_click(|_: &mut State, _| Action::Cancelled);

html::div((
html::div((clear_button, today_button, cancel_button)).class("actions"),
navigation_bar(state),
calendar_grid(state),
))
.style((!state.popup_open).then_some(style("display", "none")))
.class("date-picker")
}

fn navigation_bar(model: &State) -> impl Element<State, Action> + use<> {
let shown = model.shown_month.unwrap_or_else(today);

let title = format!("{} {}", shown.month(), shown.year());

html::div((
html::button("<").on_click(|model: &mut State, _| {
let cur = model.shown_month.unwrap_or_else(today);
model.shown_month = Some(shift_month(cur, -1));
}),
html::button("\u{00AB}").on_click(|model: &mut State, _| {
let cur = model.shown_month.unwrap_or_else(today);
model.shown_month = Some(shift_month(cur, -12));
}),
html::span(title).class("title"),
html::button("\u{00BB}").on_click(|model: &mut State, _| {
let cur = model.shown_month.unwrap_or_else(today);
model.shown_month = Some(shift_month(cur, 12));
}),
html::button(">").on_click(|model: &mut State, _| {
let cur = model.shown_month.unwrap_or_else(today);

model.shown_month = Some(shift_month(cur, 1));
}),
))
.class("nav-bar")
}

fn calendar_grid(model: &State) -> impl Element<State, Action> + use<> {
let now = OffsetDateTime::now_local().unwrap();
let today = Date::from_calendar_date(now.year(), now.month(), now.day()).unwrap();
let shown_month = model.shown_month.unwrap_or(today);

let start = month_start(shown_month);
let mut day = start;

let weeks = (0..6).map(|_| {
let days = (0..7).map(|_| {
let cur_day = day;
day += Duration::days(1);

let class = if cur_day.month() != shown_month.month() {
Some("not-in-month")
} else if Some(cur_day) == model.selected {
Some("selected")
} else if cur_day == today {
Some("today")
} else {
None
};

html::td(cur_day.day().to_string())
.on_click(move |mdl: &mut State, _event| {
mdl.selected = Some(cur_day);
mdl.popup_open = false;
Action::DateChanged(Some(cur_day))
})
.class("day")
.class(class)
});

html::tr(days.collect::<Vec<_>>())
});

let header = html::tr(WEEKDAYS.iter().map(|d| html::th(*d)).collect::<Vec<_>>());

html::table((html::thead(header), html::tbody(weeks.collect::<Vec<_>>()))).class("calendar")
}

const WEEKDAYS: [&str; 7] = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];

fn month_start(date: Date) -> Date {
let first = Date::from_calendar_date(date.year(), date.month(), 1).unwrap();
let weekday = first.weekday().number_from_monday();
first - Duration::days(i64::from(weekday - 1))
}

fn shift_month(base: Date, months: i16) -> Date {
let mut year = base.year();
let mut month = base.month() as i16 + months;

while month > 12 {
month -= 12;
year += 1;
}
while month < 1 {
month += 12;
year -= 1;
}

let day = base
.day()
.min(days_in_month(year, u8::try_from(month).unwrap()));
let month = time::Month::try_from(u8::try_from(month).unwrap()).unwrap();

Date::from_calendar_date(year, month, day).unwrap()
}

fn days_in_month(year: i32, month: u8) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
29
} else {
28
}
}
_ => unreachable!(),
}
}

fn today() -> Date {
let now = OffsetDateTime::now_local().unwrap();
Date::from_calendar_date(now.year(), now.month(), now.day()).unwrap()
}
102 changes: 102 additions & 0 deletions xilem_web/web_examples/components/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2026 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0

//! This example shows how to create generic components.

use time::{Date, format_description::FormatItem, macros::format_description};
use xilem_web::{
App,
core::{MessageResult, map_action, map_message_result, map_state},
document_body,
elements::html,
interfaces::Element,
};

// These modules could also be in an external crate.
mod card;
mod date_picker;

const DATE_FORMAT: &[FormatItem<'static>] = format_description!("[year]-[day]-[month]");

#[derive(Default)]
struct AppState {
clicks: i32,
card_collapsed: bool,
date: Option<Date>,
date_picker: date_picker::State,
}

impl AppState {
fn click(&mut self) {
self.clicks += 1;
}

fn toggle_card(&mut self) {
self.card_collapsed = !self.card_collapsed;
}

fn show_date_picker(&mut self) {
self.date_picker.popup_open = true;
}

fn close_date_picker(&mut self) {
self.date_picker.popup_open = false;
}
}

fn app_logic(state: &mut AppState) -> impl Element<AppState> + use<> {
let counter = html::button("click me!").on_click(|state: &mut AppState, _| state.click());

let card_content = html::div(("Some content ... ", state.clicks, counter));

let card = card::view("Card Example", state.card_collapsed, card_content);
let card = map_action(card, map_card_action);

let date_input = html::input(())
.attr(
"value",
state
.date
.map(|date| date.format(DATE_FORMAT).unwrap())
.unwrap_or_default(),
)
.class("date")
.on_focus(|state: &mut AppState, _| state.show_date_picker());

let date_picker = date_picker::view(&state.date_picker);
let date_picker = map_state(date_picker, |state: &mut AppState| &mut state.date_picker);
let date_picker = map_message_result(date_picker, handle_date_picker_message_result);

html::div((card, html::div((date_input, date_picker))))
}

fn map_card_action(state: &mut AppState, action: card::Action<()>) {
match action {
card::Action::Toggle => state.toggle_card(),
card::Action::Child(()) => {}
}
}

fn handle_date_picker_message_result(
state: &mut AppState,
message_result: MessageResult<date_picker::Action>,
) -> MessageResult<()> {
let MessageResult::Action(action) = message_result else {
return message_result.map(|_| ());
};
match action {
date_picker::Action::DateChanged(date) => {
state.date = date;
state.close_date_picker();
}
date_picker::Action::Cancelled => {
state.close_date_picker();
}
}
MessageResult::Action(())
}

fn main() {
console_error_panic_hook::set_once();
App::new(document_body(), AppState::default(), app_logic).run();
}
Loading