Chandra
view release on metacpan or search on metacpan
examples/multiwindow_example.pl view on Meta::CPAN
debug => 1,
);
# Persist the window size via Chandra::Store
my $store = $app->store;
my $saved_w = $store->get('window.width', 600);
my $saved_h = $store->get('window.height', 500);
# -----------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------
sub render_todos {
my $theme = $store->get('theme', 'light');
my $is_dark = $theme eq 'dark';
my @visible = $filter eq 'all' ? @todos
: $filter eq 'active' ? grep { !$_->{done} } @todos
: grep { $_->{done} } @todos;
my $done_color = $is_dark ? '#666' : '#888';
my $items = join '', map {
my $id = $_->{id};
my $text = $_->{text};
my $checked = $_->{done} ? 'checked' : '';
my $strike = $_->{done} ? qq(style="text-decoration:line-through;color:$done_color") : '';
qq(<li>
<input type="checkbox" $checked
onchange="window.chandra.invoke('toggle',[$id])">
<span $strike>$text</span>
<button onclick="window.chandra.invoke('remove',[$id])">â</button>
</li>)
} @visible;
my $remaining = scalar grep { !$_->{done} } @todos;
my $total = scalar @todos;
my $all_active = $filter eq 'all' ? 'active' : '';
my $active_active = $filter eq 'active' ? 'active' : '';
my $done_active = $filter eq 'done' ? 'active' : '';
# Dark mode colors
my $bg = $is_dark ? '#1a1a2e' : '#fafafa';
my $toolbar_bg = $is_dark ? '#16213e' : '#fff';
my $toolbar_bdr = $is_dark ? '#0f3460' : '#eee';
my $btn_bg = $is_dark ? '#0f3460' : '#ecf0f1';
my $btn_fg = $is_dark ? '#eee' : '#333';
my $li_border = $is_dark ? '#0f3460' : '#f0f0f0';
my $li_fg = $is_dark ? '#eee' : 'inherit';
my $footer_fg = $is_dark ? '#888' : '#888';
my $del_btn = $is_dark ? '#555' : '#ccc';
return qq(
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: system-ui, sans-serif; margin: 0; background: $bg; color: $li_fg; }
h1 { background: #e74c3c; color: #fff; margin: 0; padding: 16px 20px; font-size: 1.4em; }
.toolbar { display:flex; gap:8px; padding:12px 20px; background:$toolbar_bg; border-bottom:1px solid $toolbar_bdr; }
button { cursor:pointer; border:none; border-radius:4px; padding:6px 12px; font-size:.9em; }
.btn-add { background:#e74c3c; color:#fff; }
.btn-settings { background:$btn_bg; color:$btn_fg; margin-left:auto; }
.filters { display:flex; gap:4px; }
.filters button { background:$btn_bg; color:$btn_fg; }
.filters button.active { background:#3498db; color:#fff; }
ul { list-style:none; margin:0; padding:0 20px; }
li { display:flex; align-items:center; gap:8px; padding:10px 0;
border-bottom:1px solid $li_border; }
li input[type=checkbox] { width:18px; height:18px; cursor:pointer; }
li span { flex:1; }
li button { background:transparent; color:$del_btn; font-size:1.1em; padding:2px 6px; }
li button:hover { color:#e74c3c; }
.footer { padding:12px 20px; color:$footer_fg; font-size:.85em; }
</style>
</head>
<body>
<h1>Todos</h1>
<div class="toolbar">
<button class="btn-add"
onclick="window.chandra.invoke('open_add', [])">+ Add Todo</button>
<div class="filters">
<button class="$all_active"
onclick="window.chandra.invoke('set_filter',['all'])">All</button>
<button class="$active_active"
onclick="window.chandra.invoke('set_filter',['active'])">Active</button>
<button class="$done_active"
onclick="window.chandra.invoke('set_filter',['done'])">Done</button>
</div>
<button class="btn-settings"
onclick="window.chandra.invoke('open_settings',[])">â Settings</button>
</div>
<ul>$items</ul>
<div class="footer">$remaining of $total item(s) remaining</div>
</body>
</html>
);
}
sub refresh_main {
$app->set_content(render_todos());
$app->refresh();
}
# -----------------------------------------------------------------------
# Main window bindings
# -----------------------------------------------------------------------
my $next_id = 1;
$app->bind('toggle', sub {
my ($id) = @_;
for my $t (@todos) {
$t->{done} = !$t->{done} if $t->{id} == $id;
}
$store->set('todos', \@todos);
refresh_main();
return undef;
});
$app->bind('remove', sub {
my ($id) = @_;
@todos = grep { $_->{id} != $id } @todos;
$store->set('todos', \@todos);
refresh_main();
return undef;
});
$app->bind('set_filter', sub {
my ($f) = @_;
$filter = $f;
refresh_main();
return undef;
});
# -----------------------------------------------------------------------
# Add-todo dialog
# -----------------------------------------------------------------------
sub add_html {
my $theme = $store->get('theme', 'light');
my $is_dark = $theme eq 'dark';
my $bg = $is_dark ? '#1a1a2e' : '#fff';
my $fg = $is_dark ? '#eee' : '#333';
my $input_bg = $is_dark ? '#16213e' : '#fff';
my $input_bd = $is_dark ? '#0f3460' : '#ccc';
my $btn_bg = $is_dark ? '#0f3460' : '#ecf0f1';
my $btn_fg = $is_dark ? '#eee' : '#333';
return qq(
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family:system-ui,sans-serif; margin:0; padding:20px; background:$bg; color:$fg; }
h2 { margin-top:0; font-size:1.1em; }
input[type=text] { width:100%; box-sizing:border-box; padding:8px; font-size:1em;
border:1px solid $input_bd; border-radius:4px; background:$input_bg; color:$fg; }
.buttons { display:flex; gap:8px; margin-top:12px; justify-content:flex-end; }
button { border:none; border-radius:4px; padding:8px 16px; cursor:pointer; font-size:.9em; }
.ok { background:#e74c3c; color:#fff; }
.cancel { background:$btn_bg; color:$btn_fg; }
</style>
</head>
<body>
<h2>New Todo</h2>
<input type="text" id="txt" placeholder="What needs to be done?"
onkeydown="if(event.key==='Enter') document.getElementById('ok').click()">
<div class="buttons">
<button class="cancel"
onclick="window.chandra.invoke('add_cancel',[])">Cancel</button>
<button id="ok" class="ok"
onclick="window.chandra.invoke('add_ok',[document.getElementById('txt').value])">Add</button>
</div>
<script>document.getElementById('txt').focus();</script>
</body>
</html>
);
}
$app->bind('open_add', sub {
if ($add_win && !$add_win->is_closed) {
$add_win->focus;
return undef;
}
$add_win = $app->open_window(
title => 'Add Todo',
width => 360,
height => 160,
modal => 1,
content => add_html(),
debug => 1,
);
$add_win->on_close(sub { 1 });
return undef;
});
$app->bind('add_ok', sub {
my ($text) = @_;
$text //= '';
$text =~ s/^\s+|\s+$//g;
if (length $text) {
push @todos, { id => $next_id++, text => $text, done => 0 };
$store->set('todos', \@todos);
refresh_main();
}
$add_win->close if $add_win && !$add_win->is_closed;
return undef;
});
$app->bind('add_cancel', sub {
$add_win->close if $add_win && !$add_win->is_closed;
return undef;
});
# -----------------------------------------------------------------------
# Settings window
# -----------------------------------------------------------------------
sub settings_html {
my $theme = $store->get('theme', 'light');
my $is_dark = $theme eq 'dark';
my $checked = $is_dark ? 'checked' : '';
my $bg = $is_dark ? '#1a1a2e' : '#fff';
my $fg = $is_dark ? '#eee' : '#333';
return qq(
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family:system-ui,sans-serif; margin:0; padding:20px; background:$bg; color:$fg; }
h2 { margin-top:0; font-size:1.1em; }
label { display:flex; align-items:center; gap:8px; margin:10px 0; }
button { border:none; border-radius:4px; padding:8px 16px; cursor:pointer;
background:#e74c3c; color:#fff; margin-top:12px; font-size:.9em; }
</style>
</head>
<body>
<h2>Settings</h2>
<label>
<input type="checkbox" id="dark" $checked
onchange="window.chandra.invoke('set_theme',[this.checked?'dark':'light'])">
Dark theme
</label>
<button onclick="window.chandra.invoke('clear_done',[])">Clear completed todos</button>
</body>
</html>
);
}
$app->bind('open_settings', sub {
if ($settings_win && !$settings_win->is_closed) {
$settings_win->show;
$settings_win->focus;
return undef;
}
$settings_win = $app->open_window(
title => 'Settings',
width => 320,
height => 200,
content => settings_html(),
);
# Restore last position
my $sx = $store->get('settings.x', 100);
my $sy = $store->get('settings.y', 100);
$settings_win->set_position($sx, $sy);
$settings_win->on_resize(sub {
my ($w, $h) = @_;
$store->set('settings.width', $w);
$store->set('settings.height', $h);
});
$settings_win->on_close(sub {
# save position before close
my ($x, $y) = $settings_win->get_position;
$store->set('settings.x', $x);
$store->set('settings.y', $y);
return 1;
});
return undef;
});
$app->bind('set_theme', sub {
my ($theme) = @_;
$store->set('theme', $theme);
# Refresh main window with new theme
refresh_main();
# Reload settings window to reflect the change
if ($settings_win && !$settings_win->is_closed) {
$settings_win->set_content(settings_html());
}
return undef;
( run in 1.033 second using v1.01-cache-2.11-cpan-140bd7fdf52 )