From c68eb06e97e8fa5fac4a0a32e6fcf1592a49999d Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 13 Sep 2025 16:03:04 +0100 Subject: [PATCH 1/5] Add cut-down version of data-list-view.rs --- examples/data-list-view.rs | 164 +++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 examples/data-list-view.rs diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs new file mode 100644 index 0000000..faf2304 --- /dev/null +++ b/examples/data-list-view.rs @@ -0,0 +1,164 @@ +use kas::prelude::*; +use kas::view::{DataGenerator, DataLen, GeneratorChanges, GeneratorClerk}; +use kas::view::{Driver, ListView}; +use kas::widgets::{column, *}; +use std::collections::HashMap; + +#[derive(Clone, Debug)] +enum Control { + Select(usize), + Update(usize, String), +} + +#[derive(Debug)] +struct Data { + last_change: GeneratorChanges, + last_key: usize, + active: usize, + strings: HashMap, +} +impl Data { + fn new() -> Self { + Data { + last_change: GeneratorChanges::None, + last_key: 0, + active: 0, + strings: HashMap::new(), + } + } + fn get_string(&self, index: usize) -> String { + self.strings + .get(&index) + .cloned() + .unwrap_or_else(|| format!("Entry #{}", index + 1)) + } + fn handle(&mut self, control: Control) { + match control { + Control::Select(index) => { + self.last_change = GeneratorChanges::Any; + self.active = index; + } + Control::Update(index, text) => { + self.last_change = GeneratorChanges::Range(index..index + 1); + self.last_key = self.last_key.max(index); + self.strings.insert(index, text); + } + }; + } +} + +type Item = (usize, String); // (active index, entry's text) + +#[derive(Debug)] +struct ListEntryGuard(usize); +impl EditGuard for ListEntryGuard { + type Data = Item; + + fn update(edit: &mut EditField, cx: &mut ConfigCx, data: &Item) { + if !edit.has_edit_focus() { + edit.set_string(cx, data.1.to_string()); + } + } + + fn activate(edit: &mut EditField, cx: &mut EventCx, _: &Item) -> IsUsed { + cx.push(Control::Select(edit.guard.0)); + Used + } + + fn edit(edit: &mut EditField, cx: &mut EventCx, _: &Item) { + cx.push(Control::Update(edit.guard.0, edit.clone_string())); + } +} + +#[impl_self] +mod ListEntry { + // The list entry + #[widget] + #[layout(column! [ + row! [self.label, self.radio], + self.edit, + ])] + struct ListEntry { + core: widget_core!(), + #[widget(&())] + label: Label, + #[widget] + radio: RadioButton, + #[widget] + edit: EditBox, + } + + impl Events for Self { + type Data = Item; + } +} + +struct ListEntryDriver; +impl Driver for ListEntryDriver { + type Widget = ListEntry; + + fn make(&mut self, key: &usize) -> Self::Widget { + let n = *key; + ListEntry { + core: Default::default(), + label: Label::new(format!("Entry number {}", n + 1)), + radio: RadioButton::new_msg( + "display this entry", + move |_, data: &Item| data.0 == n, + move || Control::Select(n), + ), + edit: EditBox::new(ListEntryGuard(n)).with_width_em(18.0, 30.0), + } + } + + fn navigable(_: &Self::Widget) -> bool { + false + } +} + +#[derive(Default)] +struct Generator; +impl DataGenerator for Generator { + type Data = Data; + type Key = usize; + type Item = Item; + + fn update(&mut self, data: &Self::Data) -> GeneratorChanges { + // We assume that `Data::handle` has only been called once since this + // method was last called. + data.last_change.clone() + } + + fn len(&self, data: &Self::Data, lbound: usize) -> DataLen { + DataLen::LBound((data.active.max(data.last_key) + 1).max(lbound)) + } + + fn key(&self, _: &Self::Data, index: usize) -> Option { + Some(index) + } + + fn generate(&self, data: &Self::Data, key: &usize) -> Self::Item { + (data.active, data.get_string(*key)) + } +} + +fn main() -> kas::runner::Result<()> { + env_logger::init(); + + let clerk = GeneratorClerk::new(Generator::default()); + let list = ListView::down(clerk, ListEntryDriver); + let tree = column![ + "Contents of selected entry:", + Text::new(|_, data: &Data| data.get_string(data.active)), + Separator::new(), + ScrollBars::new(list).with_fixed_bars(false, true), + ]; + + let ui = tree + .with_state(Data::new()) + .on_message(|_, data, control| data.handle(control)); + + let window = Window::new(ui, "Data list view"); + + kas::runner::Runner::new(())?.with(window).run() +} From 386e98912b62babcba788180e82102915de213a2 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 13 Sep 2025 16:11:01 +0100 Subject: [PATCH 2/5] examples/data-list-view: rename types to avoid ambiguity --- examples/data-list-view.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs index faf2304..4a2a9b8 100644 --- a/examples/data-list-view.rs +++ b/examples/data-list-view.rs @@ -11,15 +11,15 @@ enum Control { } #[derive(Debug)] -struct Data { +struct MyData { last_change: GeneratorChanges, last_key: usize, active: usize, strings: HashMap, } -impl Data { +impl MyData { fn new() -> Self { - Data { + MyData { last_change: GeneratorChanges::None, last_key: 0, active: 0, @@ -47,25 +47,25 @@ impl Data { } } -type Item = (usize, String); // (active index, entry's text) +type MyItem = (usize, String); // (active index, entry's text) #[derive(Debug)] struct ListEntryGuard(usize); impl EditGuard for ListEntryGuard { - type Data = Item; + type Data = MyItem; - fn update(edit: &mut EditField, cx: &mut ConfigCx, data: &Item) { + fn update(edit: &mut EditField, cx: &mut ConfigCx, data: &MyItem) { if !edit.has_edit_focus() { edit.set_string(cx, data.1.to_string()); } } - fn activate(edit: &mut EditField, cx: &mut EventCx, _: &Item) -> IsUsed { + fn activate(edit: &mut EditField, cx: &mut EventCx, _: &MyItem) -> IsUsed { cx.push(Control::Select(edit.guard.0)); Used } - fn edit(edit: &mut EditField, cx: &mut EventCx, _: &Item) { + fn edit(edit: &mut EditField, cx: &mut EventCx, _: &MyItem) { cx.push(Control::Update(edit.guard.0, edit.clone_string())); } } @@ -83,18 +83,18 @@ mod ListEntry { #[widget(&())] label: Label, #[widget] - radio: RadioButton, + radio: RadioButton, #[widget] edit: EditBox, } impl Events for Self { - type Data = Item; + type Data = MyItem; } } struct ListEntryDriver; -impl Driver for ListEntryDriver { +impl Driver for ListEntryDriver { type Widget = ListEntry; fn make(&mut self, key: &usize) -> Self::Widget { @@ -104,7 +104,7 @@ impl Driver for ListEntryDriver { label: Label::new(format!("Entry number {}", n + 1)), radio: RadioButton::new_msg( "display this entry", - move |_, data: &Item| data.0 == n, + move |_, data: &MyItem| data.0 == n, move || Control::Select(n), ), edit: EditBox::new(ListEntryGuard(n)).with_width_em(18.0, 30.0), @@ -119,12 +119,12 @@ impl Driver for ListEntryDriver { #[derive(Default)] struct Generator; impl DataGenerator for Generator { - type Data = Data; + type Data = MyData; type Key = usize; - type Item = Item; + type Item = MyItem; fn update(&mut self, data: &Self::Data) -> GeneratorChanges { - // We assume that `Data::handle` has only been called once since this + // We assume that `MyData::handle` has only been called once since this // method was last called. data.last_change.clone() } @@ -149,13 +149,13 @@ fn main() -> kas::runner::Result<()> { let list = ListView::down(clerk, ListEntryDriver); let tree = column![ "Contents of selected entry:", - Text::new(|_, data: &Data| data.get_string(data.active)), + Text::new(|_, data: &MyData| data.get_string(data.active)), Separator::new(), ScrollBars::new(list).with_fixed_bars(false, true), ]; let ui = tree - .with_state(Data::new()) + .with_state(MyData::new()) .on_message(|_, data, control| data.handle(control)); let window = Window::new(ui, "Data list view"); From 8ff604d1a3fe81690cf98e4f8b4c80abdae0cf7e Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sun, 14 Sep 2025 10:19:23 +0100 Subject: [PATCH 3/5] Add data-list-view.png --- src/screenshots/data-list-view.png | Bin 0 -> 41543 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/screenshots/data-list-view.png diff --git a/src/screenshots/data-list-view.png b/src/screenshots/data-list-view.png new file mode 100644 index 0000000000000000000000000000000000000000..aaedeb1df2620b22f3adbae9b21c465bbdadf957 GIT binary patch literal 41543 zcmbTe30%!@yEVSan39S#DMCndX_BImCMBXlR5Xt&6`C|_LXsw_L?NL;r8KF~q$CZR zgeFZiY5cFd-}5`q^PY2_bN=t!XS3PW-rw&%T-UnRTGt(D?rQk6{j$NW6JKRV>xu=<*Kip&Fl$vBY|zpkfBfzQ!<%)?w^TY#=kOLW zv|LJ@Xlsp$e5zw*J^a8b%v>-b#ZZVUEST=3U-nA6>jodx7qiDuAUzCUJEMn3YVjC*ADU={69P=UEC!g zKu7Jjytg(!LWiEdetV#IM5<5PqeoZe5;qhc=h3H}HuA_jI0|^7i#zv17-MF3!d2 z)&A?ZTzm3_cg4z;-PxZF%RW6mnG&02rKqUb{L01dmDI@6`_TPs(=)}*kKvNEb=%a%15D<~-$mtWg-N7_yJ_57s%cwfDI>zlJeLPGWzFDe~B zek<$!&w|3j8?mwL%*@OzA`kI8JbiGNkzPw*KgXKo1GAKK=U_5z>`2j~NAM6$N%4{K zJ1qMwH|E&3Un}uhZ8F%D?mC$M^1eEaj)6f4|LOe0!F+Q3QSvFrjs(-1208AVhzm^Z z`B=Mym9aUbT`5uX-h&59dS7_ucDR{A}6dlNl8?vEnJ8Fp)~P z^L;gM6#Lmpo5UwrF+o8= zrwT9ayX(2&rWP%{a^&?a|BK8_=bX8T4n;pX@A&;L#x*hfO3RMmOKGT*x#mEg$Hc&i zHx(7eC0AFv{)jdcwCj+b8$7cpcmDj&v&DHT?(-9$HFs~!y*Lz;MZIa$roN8C{b5`7 zugbF5px96T)FoFP!L45UwLF-9^X7odQ^RW~0_ZsX09|>8J>08yo+4l*&u+)gL^IuMOv`-z>#PFGhxX zx}oEh+h7x4BJaL^n^x0t_{PSv8D1XRhdKRt3Fn^IQ%7Obku&l|KsY%I_Yxu_JT5tbB?^Ho>5MmIB~Eioz|S@ zLB7YrAG0gd-2oEL0~;GYepGaJ8R;mm!nLcCo>GOVErFHd}z)lKV|Z$@bVOwurLD_jlUXqgbulvZQ)k3}%dZE>3ycJ2`E%Ngl)nzA5!vcOx>=Z=1aTn(rMQ9c19-&r^2Ya>O#%~$9L@7 z)$=}bubi%fgOIU_3H`>6J1qA)|N3@BEmll@qu8?Ickk9B)FtiPFE20u+^mjc4l7Bn z<;HepLB}6zA)K-tV`F1)>*_WSXe7vlig#XN)X>n7W1&AFHD2Qsv)9@(+w_5BPerI) zv-HKEcS_n=d_>|D!qfJ<3{xn#RL)2I`rTW%efxGQKR-XR7wlOI5)%(K=Is2@dQ_E( z_mr1v6o1L5Pn(R4jMl7O>m3!fc@+)wRvXJ(7~ivV(_>_P7@R)6jKadgLc-9OFJJE0 zC}L$gbQOD>3dHgm7Mz;?9Z0{OLfI@}s4&t|sCDev)#m0?yr=Uwy3dT4Opo>4xN*Z9 zbMh52XZmMm-DsWXg0`8R!gwkfW7Fz8-dHSp%)6c%Y`0>)N6fAh>v10Q+MH1 zM_HVt<2}vYqve~=Y!wzh<`i_AbM@-gWc>AC+J3qm-C0qgH0?Mw+&=ar*6wSp-Rly^ zx&r}ZaJjhTrgwHt6)hgP__Ns?dz#EQW~}$SQ_}k4@^Z!8x%Mw=DKh@P>U{F8N6NT& zL@hXX#$NM|=o4}pYTZXhN;!<3K3zTUd=-;#K!C}gU*D4^PrtgjsnCh*7M<5(%)=c{fTHKuBfPE2fSXS3i%&OB#kwq0C&Ggf7DrWu`>P3sPuCf~=8_lj9J zd+oaGT6#}$V%O2TAh-b4~a@6 z%8xJ{nmmz#!NEQT{FV0(`P^6cs>pckMFf19Cx_%uonrp=>(>U%AIE`9_g=hUK*U}{ORKoJFz-j^ zy1dPQOh*(JO9}AuCICdS>P;7WW-di<_y# zcvIJbw1*ouZStd&ozpz7tZ)V02CHSaC z^Z)$3rSW@J)L8BQk>buyPQ=4<{5M6Nm-}D9;(?oAdwLQzcWY{CZA&K~O?7ADRU;cK zE4d}R*MC-Fv~pd?xRgSemz_L$()IFXlQ)6%NQ4o#N~RaF$!S=GR}6m6sEpZXWsm(Q9`E52VjyNh4gt$~zQ={IxqAE%t zAU!=js~MAF-1YNkNNVbCi%7+Joyt#<(a}Mel+yR_t&5Ik=?^6qwUze#k_-w=H4#?j z`hzI#gZ1AHI2+LMY%L{T%K0j?HIu#?GTCrP;aTN=X4(A%(A$n}aBy(Av)_f^ zwmsi(xc!ytV|}OZ?>7-3jUBO)B0c*v6Z^#GLXnZ7AqTg5j4u|)&n|PMztz1N4Y1W0t2|ABjLbhrpmA_Wn(>+LT8I9(|`UL4K$`D z1;kXle*pmDl9A!)^jthpd-p)FQJH_q=g*t5qJ0Ys3!Of4b2OcKcr*bCrs&+>v;7Sz zr;0oZvre6@c4KL^`{!l|?Jyi{&9*vx`0znJJ=Sa2u8kteaY;xpBP)xf7G;M>nqIsp z2JGqo?c2HW;dZt|hYqEtr5ys?8~&085tSb*I6X{div`+6d2pEOs$%pl&A_6`UWxXy4K2o z!f6mFX*VjYj@Ttlp$a{PQqb%(QNyO{XpX$ z3&(H0y=5OiGFn(zj80BgAd4l|n|Jm01|%zoQxCCk*^;R9n4XzA@a@~fsh=$I(7N7{ z=eP^dtN!6pP9S9Jl9H0?x!-lq&z3Ade&WR1p|ce|re9gZA3Ht-gSAxsNumoPaR(k(P1OaV;+1Fb4Ro*d)9vq2m zLM*=8*Js|`(qdE_Cu#5CP?A|EvyOsrwjq0^7m{EiB66})=yKN8{s_iKz%dOE5LdWG z{@k1yYJ1r|FhFf(Wo2<9lX~4cA+uWcpY5+UZ`-y_?$50~+M|T_w5CP4|SK z%C+D2;>8QWb8j~!eraoC^Y->8%gNO>*PP9f9nicmFeK#7z^BKducJPFSQu!@+;wU4 zXVBBruaeFbd8pSO;*_s@yl?(N)tw=`&ex%+j=$pm%GL=K+?y)kXzCRFbw}Lx%RvQ& z6_Y<(d@MdbSZf2ciJQ81;<3)!^E1qjynK#xg$)hnq7s+J%_CU)H|^Cqi?p+bj&Ab- zh1=J!d%1MDtwwqWr{U>IZXQVNQ%g)>;9F_6yWLLq_6ILqxDde-cbi?-gS{+(PTAi6 zS!w}->8S!&A$-feyL6@277D3g$2(eES+NHQs8w&v`S$Hy#8T?mcyH|D)xf|tg_(zb zNIW&*SH5&f3U|nzekrwSGHM&mfsCf{SbmcnS&umhc6Rm+3=B!))~xUEm^`?DzZioS zvFj*P_KDpa)pI^$+AX4cFz`S{ zR+oHZsRce=P9fWoAkb_>*|||FTRYZglGzJh%QdU}E%52s3-@@6QAuSzZ+S>G6LvWqO7{;circ?P&dwvcL(X+CW z>nO~0U=3B;JvBM0q@l6?>({RY7G|ZTrNc zF$$KRt48k1m4d8CK|f~L zlsGdB z)Z3?C6ww?IFqujJ&&WBX)dLVQFes>`I$C6`WHskm{1^NTuJ|CZxZ$P#o!td7gLWo$ zZrBFRAlR7r^n8KTw{G1^pB<#pe;Ssn$YjPtt=We)Gzig?W7DP(wdst< zpYvBxfAlq_v&sDV#>g&ng&A8-0nm9B9S3i#i1Zs;`+`uudC z8+Ae6LX+_L9SQ-*7!RlZhLu3}+m2~#)3dXO%+F2*FQq(|L8FA#Pn9r_9j~|ozir289nRhUjfRA9ps_sYWg{rc< zr)N6>%$AI%x=cZVtr_5`k_nBdrl8Ph3QC25cc${<|Ni;Yw&lJ59X1cS85Ok}ydV>D@bYw}>_Vcr_40#={#zs7-7jCY zho8R!D>9{=;)R7Tm#BF-*vQ6)Hd4h8(M*+0ePAkWv~-ypP<8=|egF-x zSSbgw$?m#-U5o0IilPxO*rBiIXQ#7TH#y|10ky)?mtqH1h@ zs3OD40JH8V1<5J<1)wzhRclzWdJQ6qVfKn{XZ=$|m(`mvc+5V=-y?lZ^gfUAzB ziCSJ1`F~+q1OTvhV@`zugKRX$LU-LP?^FK)Fk zs$+g`ZnQ?clTeC)>Ap(I-U$huVH^i|Y*fh7sS$MZ|BWPR@9gXcsF3wY7dueV+&ndv zS5$Q4#fulG!S4C79Qm)rXe>y?B<%9O&lzVTq}(Tm+k=6#!Ckf9V|kCAZSw2eyVBWv z_qM+MegUg(`>tJ;sdd;y3gfxLZvG!jN|wEQ_b&C(qnkSWRA&?N4@EdY+a$VoQ@610sZj|rTznlUdC=xOYJ^c+1^@psX zq8Gj$zBhJ=L;m}>Z)Zz=*G@0Yj&zTVgcCjsTWuORI7Piir52Q!Mo5L3%OeLB&4KL|3zR%vN=6up}xvCB`4cQBk2 zW?8kKJrMW~QD}M9ZN9Qpt>iA0*eoKZhhs_cd?N@5HY+Em0(W9$XBYnAgC;ngd#_&` zw$cz>Ap1AVTio!BcZ`kijH9Ma4xDGW^f37CFt-~!PnQfxUPHJTNrWFFc&i+Qvq4(3zEon|l>XYO-fgM}w5`28^M2Wn^Sz zNs=pw_I@W4r-u(KMyR3(;scx8g)|P{lka9k1c;1hHf>_07NroT{Ab%sI#f@mUfOQK zR-@|qm1-BgvpH+%D(rXiMkh{e@|fx0MLFo^miM8ezMh`)A=B)r)9@GFHL<7#@zBvJ zU{+Arq7U4}!^30W`-p0Ie$J{mYfv|ogK96O_}hnf$cI~w;gT$^tWblG{;rcvMiJ7g zaE~^jb&)4h#c8lf{SJNh^XJ{x`1DixMw!zEg_;XheB~LQg7SUmx^^V3e~6pIW$oLy zFW0emz3rFWZH-v9C8(Xv*`@dLcx*Ymj+$B%8Y%^)wSv4n6)Jo`)e$ixHdNIT^i;Y~O=I4~j`JhVSA zv;g?Oe*IcA?Ks5?ar0JWWFpo!uH=KcD8`6!OO+;64ZuISTpQIPw>a~|T9L3R3J%aq{*fNLUj7PMN)@W%YkZxQ#TEEcMAX)}I52VhefCLWnZj@vxGB zP{63?fOl~+?s|IajobqR1DYU56Fe94P8eycw#fhui`#YhVrd{>j;?qW8XD@vn7nb* zCPEjWgk=Td;!Ec~&}^meEkjQf& zhVEquv^(lwZ=_!N{{8#wkcswLHEk6X+yE?wLU1+o82kAd=lJBsN1$f_YLrPaNGk+mKK}0-kzt zZtVlL=&RtBH$aO|nm3QcH-Kxvt|*{lmzn9?{_V#PmDsg}4y0uhJKJ#o2q7>~6DnlN zxgPFh$X0H(+tAR!?|zPF$b*%YRiX2Eqd{MFZEb}zr@QYS?FSH#7{F+P@1oDP81DZ` zI7Trsrgcob#UgdmK5oB%be~zA*^GKhu`NG8KM6O6#7%Vg4JgKn=DrJ{{$m`<*!lhY z_rq#xg!Ea0Tkmh}aRuvnBR-xlOXxiNzH>C6A3b_h3OM~776H>rJ?)t%JN?4~{O8mJ zzlppQ&53E~R2YhHS>9V7yx9SIV`0YEuf|$!b8}Z9$#U`W(Sm|Mm3Lt$@a){&oc(y8 z*1fSKcp!GyzovA7Q$FP6BntXU5ad^GZf;kLS;9(6;Zf1M`(4%} zcQEg>yp^SL_N`Ogb}7v;^I$cs2eHjv2QV9ATWql`R8}9$;Suk zYpAK^uOhe4qSlcyb3kI&(>_!#vDmhK@zT`TcL%HwpwLu z!;FJn^MbrR+Lf@UCsoKK7ohOlRnpi)Y%`l5_u>KM$xl{MV&U#-;QBdpt z%+trf-5qGkI3vKfijw+ifO~4<-w=oK@mr)19H@VMQb1z=elWD!S%v(eN z5D|#jK>x)6cYYTr%`fdZu{pR3=s=c%lHI_<5(Fyz9*9_a%B;& zQ{0?&fuwwb66OR>! zQvL~XS%st3^x74$z~CY9%YiN9s!B@f)s^eIdU||byU(hK(?|bhFi<@IPk+HRlo5%z zx^xcZP?oZ(kYMG37a2h`#iv$bPWyhQEN~T?PBI)KPvM_Z9j0Jy=%e>a*qJD zE&xNOY0Yw00Y=%7*AIVwzaR->@fnN)MmNl~E4AbgCrFDsJ=vF2=ag+KeJiZR|L#MX zn$UEc`c8$Sl@OO=e8kRuYHCt>tatuD2*&(p*BvpHN};SNQw#yys@=nmwJtl{sr?K* zqU8mFQ;v5p4eZH%^=iAU?3vwFwY5yc^bKa9I2yQ;l&{*3UT|{CdV78XrGQGX0(?U& zp#JJ))aU(o6sRCX7mJUk6Mgkm2so=&Kl|^>iO>G7sv*h#g6E#>OR1~g$H98xZpC!^ zOzhi+n1yR?-b8#veb$VHyrFA7hjbu9n)v>=P8)S}bo#)BOF0khxFsneK@1DI7rs+J zMb-3e6C>mH*46dNa@-MX1=3OEYsEG-w1*En+>f${fC^25N-J9Emdef?yUsA8kHP|h zfJ=1q+q}BQ$Gvj0v;E@Zu1p=DF-d#&q({0kIkxaHTA=lsPAyIrK+qF0kD=Kvqn@b0|oYv zvS$Cqp9eu1=2*8Fk4C(MUVy6k#UZ*W8+h5eySqs;gedLfF+Y=tZ(cIH&}PhTlioC` zdzpi-&C!!FTjv#B++X5-`fG#4V`+j`O}zJ0QnrDEC7ec)$Nc>owIugk9bmESh#uRf$2 z1$zm!fM=JdZ0q@i5V2RC{@V1ix&e_m@i@;!Z5#-A?eQJXz@o2b2KHhB2H!o`boZeX zr&-y6D2{t9S2I>>X;+TkW^Oaf=auA$nTb~Kw@aFQZ#`vi8Ku}z`Tjk% zjEqc!u@CagRwQ85P@`jG-s$Ol@aFN@H1Ymd0I^&xPL!LN3Dw8#iwzFcW#0 zxGESJ8Hp`PMO^4H!0%VYOlEOgc2t+Br|4lIgQw}5>E8c=JM}|d2~ze2-JYAdS|kQ@gD@;SNOM{`UJX)1sye|0fZ;=FCKFOw1#fp(APkhJF=D zE1Dm&%k`L-LKv+_(Lq?v>DkW3Ft|sG>+7}5KNnth*&V6!ro8+Rf0W6Gdxrqni~nu) zDYagWU0HognZB`gLjQ|zuI2+~VkC9#&0JY~`?ex0Si{$|!}~3sAL7=C3lu1tV<04{ z#$D;4>(@6VFLW<2M#<>_W$~T^$O4QvD*ck08d{iLN+3LwZ36kJ8}i*g*V6?}%)!BOKF1k?J9&Clhh5RZA(;E>HK18~1YkT!BVoV{NVej6( z&?Myfqsp*Rzys!jk$f!oP}KS&JPvDOTcc#Ow6us#@OoTa2!Vgdol0=VVW(fk9#V-7 zCKRA$HY+TE6+kZcumvvuJOwe!z;5)2ib`oHn>bMwj~_oy%6O>#>R~3}3JCQJu!HIx zHqE_V!^2^q6)@EFtJkv^=Q|G)^%Pz_;1ArmdKTRs!LS$;8oRLP?%e0o#y+qoM9Oz; zWO3cDs6Z)>JOlnK6X&qMp}uU3c;_2k3kg3!rus7$z7e2j~@UZ;iCE|3MrS+P7?x>fCytHZDIi?n0iy zi16W}O_3^}eweUU*5{NjJP1oITdl!$>57Fw1^~x8qs*sI*JF{ECUxWQq~f&wRevPv zEE(vd83~u*ig(G+jBQ}iX#W6O{6`tcfNrah0>T|i)LfGR zt^ld78|EuO_+7-J3-bvKWI9>xs8qVKr{Rx+jdNWawDr@COFU_-PVCBBWcIJ{X)wdA z_78YI2UZ@OFn#vrnf`}}=z&WN_~@VBgyf{8<%B^yc8uY!`=l;xVqc3E=QGS-Z8mgW z25O1o)qD5ydT117NF=1j(%Mh1-BRC%N#ZCdbMi)jm;Qx?2Xvkq1VQU2Ba(EHAfQi= zHvDaz?>%raYXlPbY*a3eZeqS^@KQWj_%PC`U+}6Z|3PwXBaHH5ul3Kbp z&(#&~WT1Gsk_CsDWf>RreQ&?&tKUhKwdK8Ub6)i`Xex>tU!*W%UPKgYF zz^L^HW@`!sffvQrUvE3W%*!ER_a)aG*1)ZM_N=A$lRJJA4smwsj5n%{o&Edx@;8US^ zAq8A}4+(Y|h3vu$ryHc;@E}5}?80>Nvja;jF^88)f>YYn3w$VROH0dcetr;u*;#8z zC_Z*Kdq@wG}c6 z0jPVITx>}f)na>KHhB7Q;*G$|H*_vm&i&dx9wwele0F~XksuC285r#_hNYN%LGL58 zsA}@Rv9_vRyu7@(n8*-n?f=x&g0==hf0YzDL&LkH2zJmNn{t4@4mvuiQjSo;;Qn9&ppMH=)E2kaL(mq0-H>)EJpW%% zy>TLU;UU7XC9Qvq4+TRE0(2VTKuc3|s}0u&|D!{mXo10g<*p8FZEm*Di3m)MgwmQS z|0p9P0RBw`Mu1`;lp=cs&H#8ih5LpIPR99c$Zi7oNk%tXx zjUocyo+?oK&wOq6f8}eve0-?TDS~C#)!n@e90zDX%0Vz8C{i@FwS8h@Siz(=e)<#? zc0Lt>5S#T<|&) z!A)Yq+xPD+f9(*wkzyf`tXlTqSlTVIPpWChsS{CQ#6sP{?a~|mnBMRH=hyjy@di~0 zPS1@44|xSa$E1Q;KKrO63x3q7R)+F#-@VjH$(lt|ieE|0NPVh?;c* zucwooM7V+pDGw6gW8qJ)j-NW8&1^w|^p^rRMxY32D5Z=)5fB^HLyo~Zh^=Ydnq>j9 zDl02y{A>dKf&6MFi38HoF|=_HU|#Uf%oIsVO1gidQG?W^KC5U;;rFfws!p84rh5a+ zoDeD}&^}%7b|q}IQASr&v4sk_R@!(Fj)={{=6O4`?-~&P1nvQNaZ^rg3CpV(Q%Ty4Dna+vYyXM$~DMOxpX6ZEQlpPz9HAQ;1y#MCA%fJr$e6?WEIIzmcBgbgjxB z+#a3;HYwG+n_d{Dt-92~Q_|F=qxY%%jupG-t(j_rgy_^{^C|BL4jU{YiWis#D8bbl zY%7q`X#3{oMItZCg2_!P?+FTgD`>+Z@%z`WF1U1Fybwk*+zPpAVPS!=g^iZk_97lw z9^h+2x<{7K{v(uadu zD&>S1eQ&Ds%(w*>)R%l`)#K|aAg$KJ^|6EJ<8Tp}WfZ!lzrI%y$041}^g-8SnGj@A z=@#GIsVaE>{YKKhvYUs8VMzHg`G(6dX`q1yIa;sGxlLUBWUJbL?ep0cLtiRC)5P4m zG6xHYFP%$x|B(0^3yUZjVT=F?3U)k}{7x_}_9TbPl%nh)G+1t~1e$OHTScdwCeg|T zpZJpUNel*X=ZT<7+hb(pQbb~PsVcn;qw^Q&NyZQ}~p>W1_ z!?!a!HC4%GSKti}IRtn(!F`ei<843Eae&y~yG#8DD~dfDGrp6uV)bg`Te>6TF7=}} zJ{YTlhD}Uu1fVUC5tIb%40Jl_dVXD9XRly5PCYxj40QJWnu*DDAym|3UmWXL3b$xk znHTLkJ;ml*-H)m->Diw7?~D{e4Un!PFy|=hU>av2Qt21NhMPZsTGK#_#cCw}R+8?a zmqVXPgijM(vEapV8x@0q8vPyf=z4$JVqc$OHrU^6i%{&CnmNWe9%{z z2q|Y9PFYq<45OSQZf`IPN}(?+#-n8_LW2C~3+m5W`6D3wXVJKE*zKghKzd62R=@LW!yaE2_fAT_zh9X9+#_?06u}V58|gW2Cp^O z6BADv*0Jvt5Eiqj`37=hKdM>A2R^N%5a>eY4WYU8`ZjsrNkF-OHdOgt$olZopV-=U zG7LjuiIQfRGN?~he($U>KR11ss)~-w0qs zd{ue}ZnKJnjX*LdM$Zm5sSZ|9KYAkEGCzB-3MJ&_jb)Qs`fjw1zazAs(ywnDqF#I2 zAGkEWq3~r+j*_P4hF`yb``OJ1HWF9poR6Z(sFYLR24ZNq6$#Y{+juYOE3ugn>XjVW z-d=>#dIS3_=x~n)v06=Q;|LL`B{?p=u|ChyjPw!Zg0q7u%=Z^Ai6o9B?@ggo4;2*@ z`sPFP^Ip5(0U@#yu)ln9VPp{&J7qofa~B&%R~K6cKVi#H0C+{GK=4&Y%I=NVxEF`V zZ&ZCheDvt%P+h)$DCxV;L6sw2PUZz_bAmH>Uqtrg)lOPf zDD>yr^tlau{4(9jgpp_h;cc~PcgavET$vZd5{VHGO-6Z zZbQrF4mQvkY0Z9|b@&^^66`6P3D$Tbc>&#%G=k0}Vt|0;08heoF&iCp`+@bg zv8TL(G+mn@8;g+3@2WC+?Ki_+*L{LSxUfmU`Yv- zw(cm%duL6#k@EJOaCgcH05Z#udik^U2g1e~Y>ymS%OUN$l}ChlJBh)ej81kdFE0)H zREdCr3=~5B0Tuw#;lU(M=%}@f++C=(0?>^Hd#Hc9{>!h}Tuj+kbtY*Dog2+9sSo!Zc6|lco5V5pPN*w#BP8tzGk3y`6ec&Ypu+xE|=S%rMdUR z+bajN?!}P~zI?t1uU~iEQ~fe?TbEVZRS+hCj_8jlmapBtZ)<@6^;||ZTJD~k@jjmiF1H2JB+h0>)YjGl7ophVxYye)6=L^h z>~AlCJoXrM8*nn)k`tw%B_kUy9trfa2a+~OD6*xW=N~+HFmU?vvH#X%bkCteb->PZ zmhI`8qA)^e|7>CG^qA!%^g$0Kd(x-d>O9i11%4=~;Mc3ZlavFu{wUn)Cino-_=0Xb z)K}MU-nLTe=8R^LdK*9WjJ3`OSFT)1lxLU@Qb5a=fW_pHVq;@N5n)u+x_Tw2IJ;^a(`>BN z%}V1&HFb5rSFbKdl)Dq=xaxc9$)jSTqQw|H=*@TfdHML<`>5Pe^D&tUr({%47)t$h z-CK?X(8b}tn+x1Tm;?-Y^&)NtO7Talap!;Ii+IM-xa;hztj{I$iM80c@f3EDH%n<#QeIQ+O7m>N2E(v^@m|Fquy zZNd9@&k<3IJg}C5^~pk$2%O6}?_*`ku|0e_Ya*8KD&qz{RG*y2ClYtWTl;d7P4sob zS-llSk2Fej@DLW+Ez#dOH2iB8D?8dN6;JG3pjxaJ6j6EWdw-g8)7YQY(*NAD<%!c9 zmIql_GBbAy&-WYa%iPkpI2xN``@Pjx*W=_#^<&3QuG4&1d_qm__^l5cZ>?s$wR)Q- zi|LOkF%@*5^qU@UOG zH`M=OE&TuetUG=`u`Lg(G)dFtq*+0p?tm}-b|oSt!R|>&fh^sK#-^sP=+!8NJ9Wv2 zuizBb@{`64K*=5lU0lIw78I|=KM9LuC-NKQdI4Y_qe z){$}vF&`SwdWsiJ9oymQMIj5qwgRLQ=e-4M9zrcSShV zKpt=JxIA)zaJ1(7VCaao07hVRyH!F&)FiC&hrtQCzv%sWd@!twXr`ea!4VbLJ{w+E z&$WJymIXvKG_?>vFtdm$4LO(wRSWt!84&hR&WpBgUmAjyF3uO#fg6Ni*9W52mGH?! zizS?ZM9&~cT%hNd1^^wNdR1{E93_c$Jzb?iL7P6s3z>yNfz@r@VbiBn2Nf0l`j%Ml zY+5n8xR`v>^+&?m>>{{UveF&&ol`ojXcbiWg}%Ae~F-4=q}l z5Fo8K6w-|fhE98$or0z_CD5BBr$KH3&8~O+=sy^(3p=SLAZ!m`l)XLrTZAp@mcFR^D*IHcJ7ig zMh_~NgtVrAJU{r)&rgI^+edFL9hStkCwO^po$O-BQmM7X-qV5#S}83peZO4CtbV7n zKjJi3Qcl(N@9g=VH+9z9o;2eRUjA@ANS2I95owPsOlMb*IL_=VI#k18G`#@nkj(7KLxh`&_0>}5QW z)OzVUuAS+gJM$+t?|V*-4+8-T$NAsy0Bg4xh!cua*?Ik@()$; z-#*_e&um0T5Nn|!UA)F>JPHj39!0!6cO8$a?*@K8fp1j^6<+qC-|2&8HXcdE7VJ`? zWe>1@AtNOHfDVzg{!nuLJwy`NONrANah3vdT+ZCY^|~T}E+JBU)o~P&pIiwc<7?g^ z9S+54Je0>ihk6XF&^6S#7L4B8uCOi);$Q^u0n}y+VCCyBGSKg%0_C5rFBQN3FV-JN zsIbDk?<A<8#@&cSmx^pC0E))U@y0!DUHp$Z1)I z)_yo^i1Ei_lUBgP&x7jz>abVBpF#>_#5X)r>EiLXp`jtjL$82<)s%a%h`@n@RtiOM z*tp;Irns;GCwl-8nn6e~cyV<7A`P4Jh}{gv**Iav&!I2v$Wr`QiiUe5Cmu zrw$0h(L&Byh&*Xn0?{ZKW~S4vac_UnMAfGleI#cEH12?XV4IU6PP_ulf>2SaD)p*R z(qn$&q*{W^7C15+cq~`UhNRlOXmw5`J%zBv>CsRMdef-LetYKFt3g~8P#q7Mo9~9v zN6@BKG=xojeXN*GDf$)nwuijsM)&~uM^5{sRAzc4aee>gf$P5!O4yirDtj0=**nbk z*g440e)(cxfC~Xf55l^X<|0sobsIZ4bkY}%n&`@vEB8O0TfTHPdR^ncep!mz>yb7Q zQl3NVPdU+e4F@F9q2a>^25L7M-A*053cU4J7zBM_X+hSoE`8Qy3i(jeRHIq+_MFic z4feSrv5L0OO-uDGRNYO}N7NcQvSx~p*qnigNhE`E z1u>&FS3z?goVRxJxhdDarR>3>Ujfd$_skVl4f)UCii!$m3g5+*wBKs1I2!G1M~@!G zYullws@~>31SSlyHUx(hbt}!`{(!WQI#|%-l_R*!VsT+U$vy2j5BY`d+o|xX7$MaO zQwYaBEd9Ao{Wt3$E+)d64?{91Y@B=3m&e$LtRlx}!MBOu;O1HFMcR7MZJs1twovu{ zJuxiu8su&O2|0~Zx_GJ^0KdyowhBS~B=(seSS@F{^zro!Uh=N3n%4EoEZi2*;5e#UTOBFi5zHa3Qc zxZTF~g`Hh5!j3#n$RL#VPSx8>{0Q-nAu6fFGPL-1OZHbB-5@M@ck8+J+Cv#!*i{n1P`H~YX!0h3}m~8CSlIsaK05LM@7s9F$ zVipkI#`b0q#J<=@?PJ0t@nQ81EmLe_)*FUAtM3TG-^Tr;E+L%QS`(0aN{~TtG#=0r z)B9zl)d9mko0nr zwjW=HzN@M#L%WUd6iAi|Fk}vZ@nQ~jo3d^E%UeR_?-|9btFnw#PCMTB&P10mDE~6J zsxZ{sZb4omCy2mWvne}D1)&ZZk7lB(U$TRU02+}?K;WqB`+z(w+lH=yO%o+nXpDrikp#t3$DbGi zdn&^>XYVVIsWiXlw1~5A7_OQCuPQjh5nW0eH=r$PN$y}|r4arA(lVEdipurq=smX{ zK6n7sBPW`gw9t`-u7T(|tNiD$Z9+0K1kk-T22OZgk!vrK0NiqN$@Sr=NEf=y3pF$*!5zjSTpZk)wfU< z=2pRwc8dco(BHOg-FmOZ%BkYbn=V9ea{3{{H99<3;q-#>!Ourqv$W95?}M{;T9!e* zw;~76;kXJ-n5cp`3s6UWq}|rRgm7BT;>g!8Vig+vgWXK~OZo!$i%9=ui~S8yWC(={ zGp3wZ8+J9A*S5Z{217tpC-9M=gPGm~tdaiYI9X2)RC?y4dN0lenR7P%db>TGG9f;} z*m6^io6&F;=>{m`OlZz3dLG)wwzSY;8CO+x`29m&OIJHLFOPi8H2NY1EgE)^!%(2& z-4VAl^r5)#C&wiD`1oYydSk_q!I52@6G5FZN<2EaL(i>u6vL--*N|G z;?CU4$7qVgIU{BD_0LNV98<_SyXIU^1^=>5i2MoT;9!z^5{W?5($4(P%J4VbCsoESS?=l%(&dxR7o ze(Z(~A?e~Xd+BT^-lF&ilhH!@VZpaO%$CA`-h67?3jgyr)F9q1bwM@r!d`YN)07rx zdH5i4Btnm37Csl4rJD@9I63bSu#hr5Jd6x&X|Ggz4F7cfT_a9f--+iOp`2g}kkh0? z1`Lf}e$a`UI7vn@E>{s%@FtiGa*hky0P1zAp=}V+1YI+7u#}+R#2-g#5p)LD0pV5| z{XdGcV#;oAk{s^iTG(rJ(*6Mb0n2zQEc1NOlalG@5lBr$LBQLc z6)hR=)}kIC-c{6-*{H=SM%y+8&L4;9qARn$Qg1z*7!5SGH(&r9VCLw)QG~3@{rT=K z?5(w^6a{gr6PlKiA=bkZO&O?jJ>ot$RtXHrK+cVVa|T|+HJ*#JozKz1gr35+L&L8| zWH2$?a55J@27;lNUU~(NGV;QCABiYvFuJG(CG5K>DCl?1u%D8+t0#54Q$u^U`c8U< z;;HWa`VE~5>7k#N_YT3O4t;|#5f;yNv8Dqc8kzt-U<;vR{Zxd<*=0n0UWA(FJyX)s zim^3xpEyCoF0lbpAKF0_AXbp`J7A}L_j6)2CK)|jc=-)jrBQP36p{fNT>-$vSw^8* z6D1HW>!dvdc0t!~HF0ZHkcwsnL>jpinN7@LYZ|w*N^=_Yn`>u`oHI_ zbF9|u)o%Fxp67n<`?|ifC| zViRzm$!}?OB$`!_^zgy_TLa&#!4j;#H&5vj6%q~NNVuJVpLgRXQJ*&J*>vTyWql`( zywPIz{k{YB_Ve(h9y)E16^Ep!6?_R>Pi<%qow&qqn-1u~G9)^$dU(5Zr@<1z0UZ`i za!h+=Ryqz@jHGd5M3~*OX~Iy1E=W{C+xcGsTs=Ze`Ee{&BhN2=h1jU**RTpZS6}4l z7LJ^7L{`IS*8AFTwEAmTquq(RmHiW1wVQEi^r%s3fThN>{bFlyRHsIFsaRo(aazRe z1`!BKmWJov1IjEMz5l}>(<+X1GpJ z`Hjv^ts6YugCvuf9-CjmjsNZ6>b(MNsRlp>egRZ7Sj_NFLFx~EExm(~JR%Bqce|?Q zV*R9VTh~!KGX;)1v9WNUBO95(Xm{*ff>#=~F%)nuZ&S$4d3N=}7p;8j|LD@$j{WJX z`O~iYPc8fZ7Pb0cQLXKk4K5_r0(}zsFZ;5`r!vQIMA4K4(p`z|3bKy}dPM&@x%Z}d z1tNbb^kjJw;MUCzde_3D>cHij9_rZ0VGczIg)OfZdH}J(BIEhQAsLgIX@?b^qiJM7 zHDyJz7V$M^ByLDuK^jR16BA9`Xnlw!Bho7GpNs*f-#_FSEq&8g;=2SkQ|BE6lO7#0U_#fVq@>}twk;%> zmgrvd136|r`wy;S6=6?sB=$DD`$h0{MQJ>+Y)yRq#-<;(#K(@<_2*&LtqHYK$+)MI z*CcO&N?^NP8X77|x!yC#(^pvvdGU=9>5CZZC79F9r}jlGX>u~cqxiZv$FAD4W#@B$ zNG<^sY2nu4THd^*aUZ3zs0ztD?o@|O-0ve-SWLMqmirS_Pk49ZC{v{xIa;5`0I+T=;kG*>3YEESf zOFh4_RFqdlMCKl}q1kD)3bI?g&a|BStg5LLV8Cwy{pM)7)bjsV3vjo?&jS{;TolPM zR`OH~_${pVO;k=w3fkJParz@7$4v*STSv4NH&T9_1>kBk{e%)iVz}!~Ir(Ej?eLW22tds2Tna%jOXL%Qa zY$y=qA?ML6w$PrzlK={|$zTj~@fhh^7#9F;^PI^)amI=*jJPk!;qm!{$V zEBkwWe%5Ms@Ca~Hb0EHgWkn4UORAyc;Bk{I()fc`BC3!<1Oms-&5N%`HJe7p<3sHo zA5`0!CO3|h7czTl@niH~={_d|tD3?xxe$A;u>O;I=?wnwktzXCs~ zuU}CNK(2E2hniZ(jLE>+#5s4WIju%o%%UQfBbt@@_m`eoP~;ykzae($iP`iwBH(~J z>>{cd!P>Y7VD_@XEkF?MRH1&DQBHs;n}z6C0&vO-d~DY9!pVZ zicAJ?ONZZns|jHKiQIhfFulAbSGIJF@+)XK*0lN>@+_XDei@;xx$ydx?si$s`BwgI zpEFe>PXDUyYPTCm*~vN3IXC}!%7d-Rn(s2~?N!Xao1TNdN8Yz>1BBLDJomplukDtH z3}|J1A^DFV?=!p}2wRtnMa(GR#FrZAb8vm1IY_~b8%I3nw8xywb&dBOq(ZFQo zhyc}E33NTE%Mt$i{me+!;#rqh*QM%xwszRD7u&vca+;Uo{-Wc*6cs>`@?+VmQ+oUY z)Yk;m{yfHWYXfS#COFJKeuw`;4_f%o%&)c*mt}Fc%Z}^T6J5Vf`c1EXc-}f!zWu;a5$j^(vR+jPE5*mFihq41rzhW>V6<-CVPEf{bpzDkvs6V@56Ys*9HY5; z4<6LRL*h1wOIp%|^~S@qmk3=f7W7YwLK2X#p?@8F0V^siE6wlQQml4uF|cTaMeb-W zdG3*yiw1CXKzE6t5juH?Vbj*B2MRBcMEnBnrN=PagSPL81F|+is$hLKCgN9})75oxGPmCX7j zJ$}0#&cqeT1vBq|C@UMnribMWOqqKnyW>DJypmh~-R^lxS++upBqZX_A>NVJ!_Cs+BHWxNrfM6TlB?KMn2&kBnIq zq|=T5msC*}2O$)l;&hthLvatfQ~kh>9e+f7=QM*z@g^z&5Vlubl-*&7H`&_7OD!gB z!mh*gYmOO}y5D=j3tfJcV2Rf|+v_jPXGb8037RU{F2Urk#`TBe_t?KOX`WCgvwgQo zze8!$1N79}Z#NI2tpw%?6EVBxR;xC5^H5BvU)!Z=*l=UgQ8M=LzMK0dIlMMaGKKI9 z)MoL2=O4d2u^{Bj4Tr~}v8KoJ$jVYB?9=UCBLDWywSVfC+*dO@hF>CfPvE?Uq7>}W zV<^yoL@4M6^LQ-1`_#pVOClAXSQa{Wc7#EY4N@I6yGp&=FBI2b{JdK>Tl)4rCDSF@%HtAac1S-%p3aOY0Lz-{OdP0~y zz~?8v!{V88jAR8E#7VZ!_q{+SB&!(E6rEp9ukWv>`PuIx`&~S4lD~-fP!gwAF+Q!g zhlI?acn0W}96C{}bH8UBT;#wJH;5xj1Bn*T$S1V zwq~}0c$hx%%a!r|-|1Y6QeE%LU(Rjf%0~04RoYM-XjGnj_>92g~}(TFRc) zU%F8xil&Xz21dK6JfnRbCCH%7SZw^f8PVwC19{$Bk5YFnYv?KPf~*G8S;TS9yae%Y zNaQiXBp@B}KfNwU)&!+#_@$X58f+d6N0gO2(H4T}3L+GgaT?5xR$fNKNEokt8RjU)>kj>Ow!=K5vzvJw8+HC4CQy~CW#j%5U+rnKl zC?v#Ry!`+hVvymw;n-7X%8<*pR&+JShUtgY0WJXw0Q0jAN`JKfLufavHbU#&-u`y3 z+s1@_7uGxUI7G+~8V<6?Iui0ja4g8XR@4cpJczUhy%Az5g025TV?L~&3b4CIs-90iTfK=j1kNR6=I!+AFH>#;|_d)#j^3@v$;{_p8 zPH2C`jt!|6%$F})Cg$klgn(MFJSd4^>FKieWt4zj|0V=y;BJX5<>fyi0~--Snm@bW|gc(GB+>kBT$eGT7Vq%J+Nx$92RE7)f27TpOf82mzOw$a>}ly1i8D*8=HETD z`TnEtZH+t>Ja$n}YAW4>|Gs!@cQ^KR7E8;(Nl7_L^QT!~8Q-;q;OFDKLO+ceoq6Jq zt}V*mzZd3B#zUmkha1^vaO;1PVE!>>5Jw5$?$cy<=Au|&U!tYM6Yfs~!{kQY~ z10qQKDxWC&%r#JA`v` ztYny;@+dU-xrckh;%o0eDAWtQsyx4y&i4|Dh=(sAAU%E=KNfWeN|YI7FeWBsT5PU}x*dPtltA;ycEjnIoyZEHhCHc6|>I zBtY%TH>VF=?A&pb$ON%YZd0#O(f|H%OG}}XPoW77^SrzE%47sJXsf{?hH`QS^;%$< zeylw)Zvc(w_IWR)TVN&kD!V&rerP|(%Pl}U2sn=Rs`??$IYGub52~X{+&N(J-|@?9 zkX1WePPygJTF#zOLk9EE_#WpXTc18?a9Xd=Zo@woe!XE=6xHt+ALQkq!`uUUv&{3O z5vO}2-IGxl0#-3>9%uaB?)$c^KNz_*t zGMm?YnbteAbgYexe&P@c>Xp{!CdZTHm_z3tTeML7kbX_GqLV`y0~BRwHS1jR0`T&U zaC;rO8SDft^EIM#Ufwu!$=xk9Mv$V+%*=m6bEs+poX5YaSRvs#px(z^bxsx<#e5Yr zt7K?u3|%y!UC(Lfb=~STfWymp_i(n`o;D}%oT}Sn)lWj`IAvGbiQo5wbHxTWo)M>v;p*eG_P#f+Yk|z!f<#_Ej@(Q*fA-U8 zRIyQ>%@g{iZ;n_*a4%k7>gsCEo4>yCGlK~98u2NPv|;V%7$cVzbc)rOPJ8J0byrSy z_UI#~-4bSAZAQ85NIU_vj)oz97pHITp7L#9$njn9GP|??CBY$3QZNvjn4@u{Xpe*ypvT7rGxme=|Auxu?(pPX{)yi?h%zt< z{-$SmooRT-822^z@%eVBR@r3cWH;7T#EX5Q>U*zogN-JIF<{`((eoQ5HLFjbrZ7Bm z;^SB=&mPq)G+ArvLctaJv?NrB%@ozF#Co${b#@G`$FwFaN0^t7FMeeQqgW*H;e`uc zKB46n?prZ{L`1aXE%C_GO|e8C9Tua6Fwd#`3uj$p|0(BO=|l_!h~;0T`AXG8AY>%Z zS9Y;B>S0~n9pzjg$!>?he=d(-WV@O@0n-xv^((dl-}!r8U$y9=O~3x@J!9pr8|XPPqpD z-!nBkTMj@z*t_Ww;b~sYD+me?3k#6zi!ewKQM-pNRK$VKB!iEMoff8#@d2j{nAcRwy+qSsEwrk^sCPQ0tPnKO4!9u1KAOUH?9e+AO2n-y&8ZiQOD4z zks5}i()YWDPJ<?yyXm<`l`DF{p*<~CQaHRhLy`Cw;*a7*tTkG*=oTp#-WLH@+t zv+B3lO^TTCZ{U!diNk=i_MKJUi>Cejd1?Ha%j~@Wqra~?d%s6hB9d>eJ0H}X*XO(m;Zxtd}noDL+d0L8{~fy)j4cIkEUACb$KU zTY|z5jqI4*G2MYfNks;t0FW}wbapXn_=N;Wy=EUuAGV@$7>EHa{@Pry*yp$QWWWl? zOjw!;im{bBUZ{-PD>%=r1o~h0#JkQut6ETek{H{xRjV#4M#e1%3h3=WRo^dE)j`1; zZnAXDsc%QdAC&lTXz1w-QWfzVFO_y7_sLvW;13ZCi9(+PCbd$fKFguYkxer|y;5P1agmenrXyYF@UmLhm|Sl5g?@UZ;jaSn6gvcL`3o&xR=4Mvj>x z;OwUEgl*1bYpgCa@B(2`M84R)dwrNBg^g1>4J4VXh>#*pEgdOW*`)h2`V%>VFm1{^-4XZp!2!KOOWCI!F7G)Q0S4>lJmij-w! zPzhrbIc6kUf`hga7=`Sk)utHYMUYiADbQr{NNr&obQ#afIpvQ`_KrO535)gAp${Le z$JM-UDp1!}L!Ze%GCBewkG4hDUikd^TJX8hbH90-_F>5F=AqGH2E=l!IC8*BL1irJNWB^wuol3wYO5_6bjlz5egFH`%VyX8o8*% zKnW8^Kxa|-f%;PBOU1TR!kHw7C`Q15pH7X^VMXh2T_Xb&2H&#R&^2?r`G3{dG(Z^y z=uiV()`DBD?8U|?!Gk0u?8~(3uBYc05D?&DPkQ!#Go{uVP!h7nye=||px}wViM;rg z*)v>>9#$vmL1xrYfJ+>QNYA*J(gmI>O%79p`ID6_{D(C`#1c)m#^b_EM1&;o$vnd8 zsPh=N~l zFQPZkkQhnCMC5V^4ZEuBVx4T~hEf*E5L4ieGK!y$h%OZmYOal}Ore>Sx)&G2jp(%c z>%ywf`Z7C)9dYW!p!Z{3oP}?4IC)mf2wk9<(f2PEX|PX` zIWILHX}aAU)DU-MIRqHSu=;39YnOCsoxk2O;-;h{;CO8jG65 z?dj)QdsnBQU8E_yVd9nIj~PMykJK zcf+XYXh~nA*m(k0$OG>`=0Nk(_kfKI#Y-;Q7fnU3q{!@u7cXAau9nz`3OSW3aemg* zQffzF*Q2o!-Pq=6a5CTmy}y(SJtX!Y8+kp3mWf_ft2<^H6UTi0`lv@w4bhD#Lnc1Y zz7I+vgL~2AD2mpqGBXZ@^?4!);OB~7j?j}@Xl|`dOM3Ou)OjS>pTv5=0s#(?S=Ke4 zJgPKsgDpFEuH*5MYevJRbK#cEutTT(dw`_?oSjF72!Dp?Z@XO>4u#I3X;O5tUyvhn z3j3V+1|PvIQCb2PrOsOB)WhzuqF_2_tm~K&dbY2V&2Il?D7VPyulJU)xrb-a zGVg_m;)}PargtPP*K1?+_KRtFoHgjQY!`)FLu6yiT~(YPe%~8XqEJ{pk%SM|T5jQB zCI8Xtl0kUI$$K<(OIx&P@x+TE&gCff#3Y{T)Jv97?B|`aCA}gAM*CSv55@t#DkEdM ziiug?+Nhdv%_P4DtE(!ImStKhE9QxAhicv=(eUR`h=lQa5&61j_PwE-L>f}4@-4cr zJw3<6;aZeIIOK@llD+bRW}1SMTRGgwZ>AnmXyN%{?m-__yjWreUnXB*u{8B(yqMvy z7_tBO?^)#%FXkRN_2}9$2YmRDeH51%lmboQm1p0=!q;8V_`mlu6){9Zk>DhrBfnz@ z$8{wtdRaPAVWILfr{ug`1fk(Wa*R|1d-n8l*Y52*Hq8stL=?J(8)L^G{O19bjcei0-7TQ)H2Wh$(Bh;=tlUk?bhI9o3?K zqX_Qu#aBb!CxTN@QxW{yEl#_n?8A!WtH)k)JA@8ez8q`)&^Wsq6_GY^cA{-$F;nCz zisbT;i6B!m;^O>NC z_*YV;o?=gA`w_;h&FhgdI9VTK?omjr4QQB%?Cd+_~ z=1sFA5|%x=k(ud7KBhoVpvkiR5S$KRI*g$(9-Sl-j5R=%$wrAJ+_~xm8)VjbJ|*8k z8XTY16!Q=Kf+#(?#e_*-eUVco$b^*OUCDx8PF_c;{jK7tS4-#FA#gubMd+`!sEUp=FqK=ezS4=wCK~{AI$`TTuOciC zK+?X9ly@CaoeGrndWXx5dyW=dce-ezEA2sg?*n*lO7MM7J~)acZO5 z+h04@9-Z5Y@zzd}2Izb691X+h200)jc1Ys04oi1-k!S*uXN?}MCc?~t=v_V$eqlXf zLg&1@cq7=ICGvaD(PV#4A=N-`hJTm7T>=AqZ0Xyn5*?;;b>mo*p_@=p=9;6@Z5F2g zDPepNI!@>@12+MPJ}BL3(sS4QvlqkPt~%T2?ly^+IjnAUE8N%zpKH+1fe%>e;Yf<8 zf+W6+cf~*%0R&JH?ftR7J*8OPdiBikX@EUI4mBd?QzkxuGg%Vs%^xIFlAkj0$EPA? z`ky*Qh?96TemK}k(kyNp-5DrdNFCu3z=B1=#%CnDpQfcP*Iwd_p#j8p0;kf+$mo>W zwCX8990C(b!A%XIl65v}=l0Hs5i1*^I3%Bn2SLVVO1LY7M0TpJ-T&q8cA&dziq6>G zYP82m%8>j@JW`AHec#=RRqeTwHb!CMocqu=aEgo z76u!-%a~1a{LG2^r)nt2CM%N(Q9u8coSYo%jtz^mQuvb$t(9tITH*CM+; zevtdG{VS|*JX5w0Pdsjpqf*#9gisc1Q4}gd2j!4>Clks8gp-$&SJ= ziz*GA-B(rydU$Kw^Q6MyaH_>q7p3_%Xxlbu)e|OD-OlFpaTxG*HKG)DZW#`Vg!>+RNQhTk{H2FDT;)#12*A)S-)0m#qX|f z*`|0Np^6!WF}`btJ8TE9Uq;>r1vLi_L%g#dpP|Tk%vlJcIEVFl>1<&)>;Er*bgA~> zFkohgwG`Ta25UEH)oO?H<^xu3u1j|#Q*oBf|1a-M`IrluyWT};YC46Hj1m`o=cWgU zul2;%_FwcX(x4;zXRSo_DkU;bxm24$H~NtnQ+x2?*v3;u%_ocnX<+&iOxSwx>T|Ji z-R=$?X0z|>TSvIQ`Wa z(HWBpEHBnmY`ahJ*<)%$Qz%pFG?L~W5d3!wVU05RYwr*J@`#>R41u;2pb5*~_h*Veymv<#udgMUtZ z5$~{GGh_6u5-ylL%!R!b=BEy}R>_@Ui=nSp4JokFyu)q6F&Wk_MHEt?k{v0E;Q++! zuRUq@rvvW)or`En{OGNC5oEWidR&lP z$8vGI=}Og-2UBYL_1xR-pl0rbBBycDwtY7IKEkuN#m3l!efG9?ztch|e!ai{!S#6) z<2DZ*9e!y;H$#UJeb+~~Y(LJ|!tD62x*atZTTX4deeL1BYtB9#e68TYO8X}Fr_}tm z+FOsex|RI=5&ftt%WMWi^>xNH$S_z zb!w_!uU;7vG(5|l?Os|5X&A-I3F9ndrWqla)NKf>Ihv75)b{UVShTt7&czvOk z8j34J+R$}*i;K&1Lk6v!SoG9qkr;b6&m+W41~t*t(2&toU51B*?Cap}+}!VJRrAXd zUutLC7tof?td-uoHu^K`(j!SG10X@vRiH7w0c(??5cifRD%!)v_kaCa>a1IdQ=iV= z^m!odz5Kgl!*Etp3U>Cn8jY#A#H$HnF2J@p8QDn+N`kM-WMMZqx03^~o}uQ4?g(ctIfyi_ zGQS)qU~JwEo#M-;*(*JXb$VTS{YYoTgDvvUI`?iZ2LgRzz?(e{>({TY;dL=SX>gd$ zxN)~GYyyZm%X5Y)h;R?L2%_K;mkbNjOta9)D;WUhKa0FwP`mGPlK_ngF>Xvg4)!Z^!d{#F_mCOwcvQfjVb589O@!g1}EG%|M($} zY6)h-mAz)P&QPSU$OUBnaF(vjE0k0YNiZZp0e#L;f=a-lC1Zx1aG^b-7AD=r-W!OP zdpa^B8394bC~ZA!TmXcSe5Hud!Dav7KzEL7rBcO&+Pz9O6v|Of2Xv&;PydO_+ zNDEscFi7DNb*1dxyT3?aCb6B(6ChODqHmQ+7cL`*AIBs}s<+l^wMlmswkB#%4b2Rj zNg3}#;uCJ^8vi*tzQW`oHl&Ea{OT^F9%=__nQ9yuyC+0F=^FTs&^VIuM)3uuKP>FW z#i;Vn1qg2&-ZnCNC^VInhtQeY=z!qV#rO?h23T{qp>)1r%a$z)fpIdP z24u@FNm#1O97KW+va&?aD|rmyhe5&R?#B^{%$zl=5RO<1X(WifQ8|dFg^<{D&({su z2#dv;%^N1I$WcF(xSH%E!eU9bPwM{9$8=EWj5V!jA+z$3@W@#Rv^N}KE{|qc_nz;d zkF_}iL>#{W%Ae**GWx4N-|{+KTLLf13lRyCfUsO|l1M{!24|4DD%>WhtgKEU9EI0o z>S1HBVo;pvtV@BLQG%c{_ThbRxLHjz-Dc&+XPL9->=|JUf1a^)-h8j-iw#fkO9}^U zIB>uvY0CG?u*ctOqq$k_+J8-R>x@3F)##K}vmQ}j!ibEz2;IKAb{-HB`PO1CwxHcx4kQlT@B38i^ za~sNFP?5h2;wtK<)Tn;^G~z9pmm~2t_owB2`v_2hPHtT)A=tirKzH4oGgbFfY~ixJ zRKaoo$*8EO-T`00-~B>Izbua} z7?n{T?}WkU?2Afo$p>S0LYiPbd4!jpVff0g@Uk~=8nImZ_U-!w)7Ow9b#v!p%O!v$ zvf2JSHe>0pA7%2g()&FKt?aZQ^z!yHNhIyK$YGw-K0t`^2dDRLjcli5Ub8EY($95; z!D@f!SiMaL+HLx7l|Le>akzN#Jw202|I%vuF5T2vFKz$D>1+7Na&6J`Y}(U$=EW(q zjjw}f9G>U%;h1YeUZeI8jdm}=ZMC>d{vcSN&|XJPfE*liruR;nsb4S5pNGTxNqqRT zRojKZdMyl=R-2HM;UTJt8P~;Vlqg1{;hRS56p7KO&Qq_)0ykZW{1v|Q-~K(fK>Z-G zD+Cob~Daz)L>=1E_#+nYvTkuRF5`F>v{(2w1rV7rGA zQ~u?zN(KxVaQ4B!{MFi@yj9y@{MCQ__ccfF_tc*v!eD#kgS}Yv-E9%V$~HxY{`Kf! z=9db@HY%X~F5-?_dzJl)z+YuvUt(yB^Jd_?kZ2jv-|K@AuF<%1U<0UT&kfIJqs2=lbW&MNOBjZH8Y+_;$JrGlx5Fd@0_Ii=Znp6B=hq+5o46 z(D2wo3QP$fpU#+~?ERdnWDC@R8$q6*RRhy=TZw?llskK-?bwKni*$m?U6wgtM zDPxWxz~rUToC{e2rXwA&mGe-U6USLv1Qskl>ViExcXl0c06CIc?HZy^1Zp54st}2q zSTWde!Y1E5zw%>GtUtWhyPS~$L?JL5NGXpG8dj*4;fPid>~Vja|G{6Mg0}V#Fjw zNB+qn+fWe=EJ+!vAc$o+5zFz8DT;`oXs;)JTXY=!P`+QX)X|cZ*-L5(fJ$Kk{GB3k zhX}LcHB^~aX9Cu08D;+Sr?J+`*L{R3b3JD}2-$)3?P4-5^lK&aR!};wcQ%%kSljM? z_lmD)<4$0&`NVm!33;kAgyU5#%Sht1F-5TuFVpbKs6&>GqKFof4JLeSdRU zz0g>r@9&GoJ|hcf6O*fCsDT)_MO{{RF|eR$a42#7PK4kvn!zhgeFaAJ^p6W)f@2#h z7+~2C+liAFgUEvCH`|FTovLT2(E@D*9MOiJV})F}8Jkw!1zZUo{BPY2)()OS)e>X* zf;o4$jlMq6YV6=^Ac3=}J*d8+{+eC;h<}&Rl?{1@gi+Ma*xbIAUk+p^`A1`O8!4RR zeaI=udWEYGU7YY^5}FR$VfnTrzp%t@*Vt7{_QUEG7wZhTQSC(Z&bQa+-jueE!3yFv zLXxYR<4ft_2ql$WY!|#^N73;Nt>(@;q)LX>yj`aa)Vd_8?LX-?qJVv5_;h(~uC@JLXBMbQp-Ov^z$hHKxa)DJs*J5~pU-FK%Qo%#aiHuN1-^=)BIC z?8ha)?6SwxAEIPB$*{gs?TGI-6cy5Q7!?B zib%(aJeyDXZ&!Djyh%dvO0LaB1YjP8+|ZXfG{Rbjf}%!fusL3);Y3x=$RIk-KA#S{IF>F2g-1@(#31R?OX1sG97vqKV4z zrTyox>FWRa-(me%cVQ<1iJ&Lt19^As7@Z(9KJbI~d|q|+zlt@+ym08(_91>&Z|H$p z_ZOiGY=kpvFu!}nZ&I3~IqglQjv5DDhN6I&n`Lr?HS-9ivYdHKC7C=WwU?SL}$Tz4BgpuLigbCQj+-Ui2W*&7f4GlO&n-4DaGSr zWm(g&Dw3lNf1<1bPN82{d6}l`hvIZ%H2QyeqZ08(^&Ab422Ft9T8eF zucR(xbnsG0+`g+?1W*RHQcIGg&~7PvVQoM~$nKR#QD@$#F$}Z&RSe+?B`9~VW(QF~AAi3&^>DbL% zfa3~dc1%qL&?fO~Bc#6;JBf%mIqKn;W9D}}Vd8U9Cdd>)t(Vphb1tr=yC;l}+(z2$ z)cTUu=Q%2jR^5qGPgJbwo7(k=)9HbOieFj%z|o`gPkQI5)0T_B3Ec+sbgqg4oX{{) zDv+0L@$s}CX>o|v8J4?gMx*dVUn_&_x9ulS-Z(XR#rniiCuTicfhMNs)N|{o`9>Z( z({5$pvndB}y@nn;<5RhLd%JxI?g55E)NQ(K2mc}}p(R3=*(YK-1Bu_Vv>o42TCXh z#)W?e?C_cIGWl$69bnxw%`CH$Br9pJ$bV3fX((e#E`!Q0xWkChFFGG0)-qDkRrMIkE^*kShC;Br>2`WZd3-;o)Aopq!N4W3*U+RmFWW_L zcIm16yB;@;`+QR`@OUD=s+AMp+;`0MDVdZu&-*=^y5Vq}ccSCtZF4K(4+LbUrQj?p z!(SWC&~XV!6M0B_m(^17aZ6-cJdh%{u^yX1)@kL5NX%p?v{bm-+SMhT7GYMkl#-F@ zl5xz@C8G*sCj51G)v!sE&n{`mg198s2~b$vIr15)Vd!c&A)NP8;0c|H7f5pHuovif z)>AmHGXSz=YOTmXI28FI@n4K7g9Bk&gu3H=&j9v0PDm#@b8>X-@*cT8c0e{$J8)R9 z86yTMN;T&Z7G}1RZYd8V5xI4l$&Oh9`1pQ_*`6zQ#n1Om8f125_q@mF^~mO{T2nm* zXtEmI5oIzhgt(8XK80kI8S7WCUZjW=CnzpHk=Lb>Pr!nD^iSe;?u{eZ52m=fqJkd$ zt*~+I;lmbel9Z>N+#ZI5n6w?ZtO;%AWp=sa%gSef9HkPOjj5u#5|*Bmxbc}>eu>mT z1(rZM->5zIIUx&<_q(8(_bwvt*BuQV8uM)Q{*xz9PGN-B6#_bZvmiQiN(R@Jqc7_2 zH6~JMMO9mQ^QXkr{?~}PM!z&5jXKj#gkOE9aW*-ziB!u`!-M)x>dqLf*xT3{tXTYZhP literal 0 HcmV?d00001 From b2dd7df469325af2e9fd949b60b4e70e41df8341 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sun, 14 Sep 2025 10:19:42 +0100 Subject: [PATCH 4/5] Add Data list view --- src/SUMMARY.md | 1 + src/data-list-view.md | 295 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 src/data-list-view.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index ce212c3..4ca7c71 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -7,4 +7,5 @@ - [Configuration](config.md) - [Calculator: make_widget and grid](calculator.md) - [Custom widgets](custom-widget.md) +- [Data list view](data-list-view.md) - [Data models](data-models.md) diff --git a/src/data-list-view.md b/src/data-list-view.md new file mode 100644 index 0000000..52c07b4 --- /dev/null +++ b/src/data-list-view.md @@ -0,0 +1,295 @@ +# Data list view + +*Topics: view widgets, `DataGenerator`* + +![Data list view](screenshots/data-list-view.png) + +## Problem + +This tutorial will concern building a front-end to a very simple database. + +Context: we have a database consisting of a set of `String` values, keyed by sequentially-assigned (but not necessarily contiguous) `usize` numbers. One key is deemed active. In code: +```rust +#[derive(Debug)] +struct MyData { + active: usize, + strings: HashMap, +} + +impl MyData { + fn new() -> Self { + MyData { + active: 0, + strings: HashMap::new(), + } + } + fn get_string(&self, index: usize) -> String { + self.strings + .get(&index) + .cloned() + .unwrap_or_else(|| format!("Entry #{}", index + 1)) + } +} +``` + +Our very simple database supports two mutating operations: selecting a new key to be active and replacing the string value at a given key: +```rust +#[derive(Clone, Debug)] +enum Control { + Select(usize), + Update(usize, String), +} +# struct MyData { +# active: usize, +# strings: HashMap, +# } + +impl MyData { + fn handle(&mut self, control: Control) { + match control { + Control::Select(index) => { + self.active = index; + } + Control::Update(index, text) => { + self.strings.insert(index, text); + } + }; + } +} +``` + + +## The view widget + +We wish to display our database as a sequence of "view widgets", each tied to a single key. We will start by designing such a "view widget". + +### Input data + +Each item consists of a `key: usize` and `value: String`. Additionally, an item may or may not be active. Since we don't need to pass static (unchanging) data on update, we will omit `key`. Though we could pass `is_active: bool`, it turns out to be just as easy to pass `active: usize`. + +The input data to our view widget will therefore be: +```rust +type MyItem = (usize, String); // (active index, entry's text) +``` + +### Edit fields and guards + +We choose to display the `String` value in an [`EditBox`], allowing direct editing of the value. To fine-tune behaviour of this [`EditBox`], we will implement a custom [`EditGuard`]: + +```rust +#[derive(Debug)] +struct ListEntryGuard(usize); +impl EditGuard for ListEntryGuard { + type Data = MyItem; + + fn update(edit: &mut EditField, cx: &mut ConfigCx, data: &MyItem) { + if !edit.has_edit_focus() { + edit.set_string(cx, data.1.to_string()); + } + } + + fn activate(edit: &mut EditField, cx: &mut EventCx, _: &MyItem) -> IsUsed { + cx.push(Control::Select(edit.guard.0)); + Used + } + + fn edit(edit: &mut EditField, cx: &mut EventCx, _: &MyItem) { + cx.push(Control::Update(edit.guard.0, edit.clone_string())); + } +} +``` + +### The view widget + +The view widget itself is a custom widget: +```rust +#[impl_self] +mod ListEntry { + // The list entry + #[widget] + #[layout(column! [ + row! [self.label, self.radio], + self.edit, + ])] + struct ListEntry { + core: widget_core!(), + #[widget(&())] + label: Label, + #[widget] + radio: RadioButton, + #[widget] + edit: EditBox, + } + + impl Events for Self { + type Data = MyItem; + } +} +``` +(In fact, the primary reason to use a custom widget here is to have a named widget type.) + +### The driver + +To use `ListEntry` as a view widget, we need a driver: +```rust +struct ListEntryDriver; +impl Driver for ListEntryDriver { + type Widget = ListEntry; + + fn make(&mut self, key: &usize) -> Self::Widget { + let n = *key; + ListEntry { + core: Default::default(), + label: Label::new(format!("Entry number {}", n + 1)), + radio: RadioButton::new_msg( + "display this entry", + move |_, data: &MyItem| data.0 == n, + move || Control::Select(n), + ), + edit: EditBox::new(ListEntryGuard(n)).with_width_em(18.0, 30.0), + } + } + + fn navigable(_: &Self::Widget) -> bool { + false + } +} +``` + +## A scrollable view over data entries + +We've already seen the [`column!`] macro which allows easy construction of a fixed-size vertical list. This macro constructs a [Column]<C> widget over a synthesized [`Collection`] type, `C` +If we instead use [Column]<Vec<ListEntry>> we can extend the column dynamically. + +Such an approach (directly representing each data entry with a widget) is scalable to *at least* 10'000 entries, assuming one is prepared for some delays when constructing and resizing the UI. If we wanted to scale this further, we could page results, or try building a façade which dymanically re-allocates view widgets as the view is scrolled ... + +... but wait, Kas already has that. It's called [`ListView`]. Lets use it. + +### Data clerks + +To drive [`ListView`], we need an implementation of [`DataClerk`]. This is a low-level interface designed to support custom caching of data using batched `async` retrieval. + +For our toy example, we can use [`GeneratorClerk`], which provides a higher-level interface over the [`DataGenerator`] trait. + +We determined our view widget's input data type above: `type MyItem = (usize, String);` — our implementation just needs to generate values of this type on demand. (And since input data must be passed by a single reference, we cannot pass our data as `(usize, &str)` here. We could instead pass `(usize, Rc>)` to avoid deep-cloning `String`s, but in this little example there is no need.) + +### Data generators + +The [`DataGenerator`] trait is fairly simple to implement: +```rust +#[derive(Default)] +struct Generator; + +// We implement for Index=usize, as required by ListView: +impl DataGenerator for Generator { + type Data = MyData; + type Key = usize; + type Item = MyItem; + + fn update(&mut self, _: &Self::Data) -> GeneratorChanges { + todo!() + } + + fn len(&self, data: &Self::Data, lbound: usize) -> DataLen { + todo!() + } + + fn key(&self, _: &Self::Data, index: usize) -> Option { + Some(index) + } + + fn generate(&self, data: &Self::Data, key: &usize) -> Self::Item { + (data.active, data.get_string(*key)) + } +} +``` + +Returning [`GeneratorChanges::Any`] from fn [`DataGenerator::update`] is never wrong, yet it may cause unnecessary work. It turns out that we can simply calculate necessary updates in fn `MyData::handle`. (This assumes that `MyData::handle` will not be called multiple times before [`DataGenerator::update`].) + +Before we amend `MyData`, we should look at fn [`DataGenerator::len`], which affects both the items our view controller might try to generate and the length of scroll bars. The return type is [`DataLen`] (with `Index=usize` in our case): +```rust +pub enum DataLen { + Known(Index), + LBound(Index), +} +``` +`MyData` does not have a limit on its data length (aside from `usize::MAX` and the amount of memory available to `HashMap`, both of which we shall ignore). We do have a known lower bound: the last (highest) key value used. + +At this point, we could decide that the highest addressible key is `data.last_key + 1` and therefore return `DataLen::Known(data.last_key + 2)`. Instead, we'd like to support unlimited scrolling (like in spreadsheets); following the recommendations on [`DataGenerator::len`] thus leads to the following implementation: +```rust + fn len(&self, data: &Self::Data, lbound: usize) -> DataLen { + DataLen::LBound((data.active.max(data.last_key + 1).max(lbound)) + } +``` + +Right, lets update `MyData` with these additional capabilities: +```rust +#[derive(Debug)] +struct MyData { + last_change: GeneratorChanges, + last_key: usize, + active: usize, + strings: HashMap, +} + +impl MyData { + fn handle(&mut self, control: Control) { + match control { + Control::Select(index) => { + self.last_change = GeneratorChanges::Any; + self.active = index; + } + Control::Update(index, text) => { + self.last_change = GeneratorChanges::Range(index..index + 1); + self.last_key = self.last_key.max(index); + self.strings.insert(index, text); + } + }; + } +} +``` + +## ListView + +Now we can write `fn main`: +```rust +fn main() -> kas::runner::Result<()> { + env_logger::init(); + + let clerk = GeneratorClerk::new(Generator::default()); + let list = ListView::down(clerk, ListEntryDriver); + let tree = column![ + "Contents of selected entry:", + Text::new(|_, data: &MyData| data.get_string(data.active)), + Separator::new(), + ScrollBars::new(list).with_fixed_bars(false, true), + ]; + + let ui = tree + .with_state(MyData::new()) + .on_message(|_, data, control| data.handle(control)); + + let window = Window::new(ui, "Data list view"); + + kas::runner::Runner::new(())?.with(window).run() +} +``` + +The [`ListView`] widget controls our view. We construct with direction `down`, a [`GeneratorClerk`] and our `ListEntryDriver`. Done. + +[Full code can be found here](https://github.com/kas-gui/tutorials/blob/master/examples/data-list-view.rs). + +[`EditBox`]: https://docs.rs/kas/latest/kas/widgets/struct.EditBox.html +[`EditGuard`]: https://docs.rs/kas/latest/kas/widgets/trait.EditGuard.html +[`GeneratorChanges::Any`]: https://docs.rs/kas/latest/kas/view/enum.GeneratorChanges.html#variant.Any +[`DataGenerator`]: https://docs.rs/kas/latest/kas/view/trait.DataGenerator.html +[`DataGenerator::update`]: https://docs.rs/kas/latest/kas/view/trait.DataGenerator.html#tymethod.update +[`DataGenerator::len`]: https://docs.rs/kas/latest/kas/view/trait.DataGenerator.html#tymethod.len +[`DataLen`]: https://docs.rs/kas/latest/kas/view/enum.DataLen.html +[`DataLen::Known`]: https://docs.rs/kas/latest/kas/view/enum.DataLen.html +[`ListView`]: https://docs.rs/kas/latest/kas/view/struct.ListView.html +[`GeneratorClerk`]: https://docs.rs/kas/latest/kas/view/struct.GeneratorClerk.html +[`DataClerk`]: https://docs.rs/kas/latest/kas/view/trait.DataClerk.html +[`column!`]: https://docs.rs/kas/latest/kas/widgets/macro.column.html +[Column]: https://docs.rs/kas/latest/kas/widgets/type.Column.html +[`Collection`]: https://docs.rs/kas/latest/kas/trait.Collection.html From 3204a3dc589f579ccd77817cb56aedf3145371c8 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sun, 14 Sep 2025 10:21:55 +0100 Subject: [PATCH 5/5] Remove data-models.md --- src/SUMMARY.md | 1 - src/data-models.md | 25 ------------------------- 2 files changed, 26 deletions(-) delete mode 100644 src/data-models.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 4ca7c71..3afd440 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -8,4 +8,3 @@ - [Calculator: make_widget and grid](calculator.md) - [Custom widgets](custom-widget.md) - [Data list view](data-list-view.md) -- [Data models](data-models.md) diff --git a/src/data-models.md b/src/data-models.md deleted file mode 100644 index 6b8dc16..0000000 --- a/src/data-models.md +++ /dev/null @@ -1,25 +0,0 @@ -# Sync-counter: data models - -*Topics: data models and view widgets* - -TODO: - -- [`ListView`] and [`ListData`] -- [`Driver`], including predefined impls -- [`Filter`] and [`UnsafeFilteredList`]. This rather messy to use (improvable?). The latter should eventually be replaced with a safe variant. -- [`MatrixView`] and [`MatrixData`]. (Will possibly gain support for row/column labels and be renamed `TableView`.) - -For now, see the examples: - -- [`examples/ldata-list-view.rs`](https://github.com/kas-gui/kas/blob/master/examples/data-list-view.rs) uses [`ListView`] with custom [`ListData`] and [`Driver`] -- [`examples/gallery.rs`](https://github.com/kas-gui/kas/blob/master/examples/gallery.rs#L338)'s `filter_list` uses [`UnsafeFilteredList`] with a custom [`Driver`]. Less code but possibly more complex. -- [`examples/times-tables.rs`](https://github.com/kas-gui/kas/blob/master/examples/times-tables.rs) uses [`MatrixView`] with custom [`MatrixData`] and [`driver::NavView`]. Probably the easiest example. - -[`ListView`]: https://docs.rs/kas/latest/kas/view/struct.ListView.html -[`ListData`]: https://docs.rs/kas/latest/kas/view/trait.ListData.html -[`Driver`]: https://docs.rs/kas/latest/kas/view/trait.Driver.html -[`driver::NavView`]: https://docs.rs/kas/latest/kas/view/driver/struct.NavView.html -[`Filter`]: https://docs.rs/kas/latest/kas/view/filter/trait.Filter.html -[`UnsafeFilteredList`]: https://docs.rs/kas/latest/kas/view/filter/struct.UnsafeFilteredList.html -[`MatrixView`]: https://docs.rs/kas/latest/kas/view/struct.MatrixView.html -[`MatrixData`]: https://docs.rs/kas/latest/kas/view/trait.MatrixData.html