Skip to main content

standard_plugin/surface/
mod.rs

1//! Surfaces: typed views over the double-buffered shared buffer a UI plugin
2//! paints and the viewer samples.
3//!
4//! A [`Surface<Cells>`] paints graphemes, colours and attributes per cell; a
5//! [`Surface<Pixels>`] paints RGBA8 pixels. Both work the same way:
6//!
7//! - Create the surface once, by the id the manifest declares
8//!   (`Surface::<Cells>::new("card")`). The SDK asks the host for its
9//!   layout, allocates the region in the plugin's memory and attaches it.
10//! - Draw with the model's methods and the [`crate::Shapes`] helpers. Every
11//!   write records the rectangle it touched.
12//! - [`Surface::commit`] publishes the painted slot with the merged dirty
13//!   rectangles, bumps the sequence number, and switches slots. The next
14//!   slot starts as a copy of what was just committed (copied lazily, and
15//!   only the rectangles that changed), so drawing is always incremental.
16//! - On a resize the SDK rebinds every surface of that id to a new region
17//!   before the plugin sees [`crate::Event::Resize`]; the first commit at the
18//!   new size is fully dirty, and the old region is freed only after it,
19//!   as the host requires. Nothing is drawn while a surface has no size.
20//!
21//! The region layout (a 32-byte header of eight little-endian `u32` words
22//! `seq, model, cols, rows, px-w, px-h, cell-px-w, cell-px-h`, then two
23//! slots) is the contract in `wit/plugin.wit`.
24
25mod cells;
26mod dirty;
27mod pixels;
28
29use alloc::rc::{Rc, Weak};
30use alloc::string::String;
31use alloc::vec;
32use alloc::vec::Vec;
33use core::cell::RefCell;
34use core::marker::PhantomData;
35
36pub use cells::Cell;
37pub(crate) use cells::write_text_clipped;
38use dirty::DirtyRects;
39pub(crate) use pixels::Canvas;
40
41use crate::error::{Error, Result};
42use crate::geometry::{Geometry, Rect};
43use crate::host;
44use crate::local::instance_local;
45
46/// Bytes before slot 0.
47pub const HEADER_LEN: u32 = 32;
48/// Bytes per cell in the cell model.
49pub const CELL_LEN: u32 = 16;
50/// Bytes per pixel in the graphics model.
51pub const PIXEL_LEN: u32 = 4;
52
53const HEADER_WORDS: usize = (HEADER_LEN / 4) as usize;
54
55/// A surface's model, as the manifest declares it.
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub enum Model {
58    Cells,
59    Pixels,
60}
61
62impl Model {
63    pub fn as_str(self) -> &'static str {
64        match self {
65            Self::Cells => "cells",
66            Self::Pixels => "pixels",
67        }
68    }
69
70    fn word(self) -> u32 {
71        match self {
72            Self::Cells => 0,
73            Self::Pixels => 1,
74        }
75    }
76
77    /// Buffer words per unit (cell or pixel).
78    fn unit_words(self) -> usize {
79        match self {
80            Self::Cells => (CELL_LEN / 4) as usize,
81            Self::Pixels => 1,
82        }
83    }
84}
85
86/// How a region is laid out, as the host reports it.
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub(crate) struct RegionLayout {
89    pub len: u32,
90    pub header_len: u32,
91    pub slot_len: u32,
92    pub model: Model,
93    pub geometry: Geometry,
94}
95
96impl RegionLayout {
97    /// The layout the host computes for a model and geometry.
98    pub(crate) fn new(model: Model, geometry: Geometry) -> Self {
99        let slot_len = match model {
100            Model::Cells => geometry.cols * geometry.rows * CELL_LEN,
101            Model::Pixels => geometry.px_w * geometry.px_h * PIXEL_LEN,
102        };
103        Self {
104            len: HEADER_LEN + 2 * slot_len,
105            header_len: HEADER_LEN,
106            slot_len,
107            model,
108            geometry,
109        }
110    }
111
112    /// Width and height in the model's units.
113    fn extent(&self) -> (u32, u32) {
114        match self.model {
115            Model::Cells => (self.geometry.cols, self.geometry.rows),
116            Model::Pixels => (self.geometry.px_w, self.geometry.px_h),
117        }
118    }
119
120    fn slot_words(&self) -> usize {
121        (self.slot_len / 4) as usize
122    }
123}
124
125mod sealed {
126    pub trait Sealed {}
127}
128
129/// The two surface models: [`Cells`] and [`Pixels`].
130pub trait SurfaceModel: sealed::Sealed {
131    const MODEL: Model;
132}
133
134/// The cell model: graphemes, colours and attributes per cell.
135#[derive(Clone, Copy, Debug)]
136pub enum Cells {}
137
138/// The graphics model: RGBA8 pixels, the surface's cells times the cell
139/// pixel size.
140#[derive(Clone, Copy, Debug)]
141pub enum Pixels {}
142
143impl sealed::Sealed for Cells {}
144impl sealed::Sealed for Pixels {}
145
146impl SurfaceModel for Cells {
147    const MODEL: Model = Model::Cells;
148}
149
150impl SurfaceModel for Pixels {
151    const MODEL: Model = Model::Pixels;
152}
153
154/// What the last commit published (for tests and diagnostics).
155#[derive(Clone, Debug, PartialEq, Eq)]
156pub struct CommitInfo {
157    /// The header sequence number the commit carried.
158    pub seq: u32,
159    /// The slot published, 0 or 1.
160    pub slot: u8,
161    /// The rectangles sent, in the model's units.
162    pub dirty: Vec<Rect>,
163    /// The first commit after a (re)bind: the whole surface.
164    pub full: bool,
165}
166
167#[derive(Debug)]
168enum Backend {
169    /// Bound to the host's surface of the same id.
170    Host,
171    /// A buffer of the plugin's own, never sent to the host.
172    Offscreen(Geometry),
173}
174
175#[derive(Debug)]
176struct Binding {
177    layout: RegionLayout,
178    /// Header and both slots, as words (the host reads them as bytes).
179    region: Vec<u32>,
180    /// The slot being painted.
181    back: u8,
182}
183
184#[derive(Debug)]
185pub(crate) struct Inner {
186    id: String,
187    model: Model,
188    backend: Backend,
189    binding: Option<Binding>,
190    /// The region a rebind replaced, freed after the next commit: the host
191    /// samples it until then.
192    retired: Option<Vec<u32>>,
193    dirty: DirtyRects,
194    /// Rectangles the back slot lacks from the last commit, copied in
195    /// before the first write to it.
196    sync: Vec<Rect>,
197    /// The next commit is the first since a bind: fully dirty.
198    full: bool,
199    seq: u32,
200    error: Option<Error>,
201    last_commit: Option<CommitInfo>,
202}
203
204instance_local! {
205    fn registry() -> Vec<Weak<RefCell<Inner>>> = Vec::new();
206}
207
208/// Rebinds every live surface named `id` after a resize.
209pub(crate) fn rebind_surfaces(id: &str) {
210    let surfaces: Vec<Rc<RefCell<Inner>>> = registry(|registry| {
211        registry.retain(|weak| weak.strong_count() > 0);
212        registry.iter().filter_map(Weak::upgrade).collect()
213    });
214    for surface in surfaces {
215        let mut inner = surface.borrow_mut();
216        if inner.id == id {
217            inner.rebind();
218        }
219    }
220}
221
222fn as_bytes(words: &[u32]) -> &[u8] {
223    // SAFETY: u32 has no padding and any byte pattern is a valid u8; the
224    // length is the words' byte length.
225    unsafe { core::slice::from_raw_parts(words.as_ptr().cast::<u8>(), words.len() * 4) }
226}
227
228fn as_bytes_mut(words: &mut [u32]) -> &mut [u8] {
229    // SAFETY: as `as_bytes`; the slice is borrowed mutably and exclusively.
230    unsafe { core::slice::from_raw_parts_mut(words.as_mut_ptr().cast::<u8>(), words.len() * 4) }
231}
232
233impl Inner {
234    fn new(id: String, model: Model, backend: Backend) -> Self {
235        Self {
236            id,
237            model,
238            backend,
239            binding: None,
240            retired: None,
241            dirty: DirtyRects::default(),
242            sync: Vec::new(),
243            full: false,
244            seq: 0,
245            error: None,
246            last_commit: None,
247        }
248    }
249
250    fn rebind(&mut self) {
251        match self.bind() {
252            Ok(_) => self.error = None,
253            Err(error) => self.error = Some(error),
254        }
255    }
256
257    /// Asks for the current layout and, when the surface has a size, binds
258    /// a new zeroed region carrying its header. `Ok(false)`: no size yet.
259    fn bind(&mut self) -> Result<bool> {
260        let layout = match &self.backend {
261            Backend::Host => host::surface_layout(&self.id)?,
262            Backend::Offscreen(geometry) => RegionLayout::new(self.model, *geometry),
263        };
264        if layout.model != self.model {
265            return Err(Error::WrongModel {
266                surface: self.id.clone(),
267                expected: self.model,
268                found: layout.model,
269            });
270        }
271        if layout.geometry.is_empty() {
272            return Ok(false);
273        }
274        let mut region = vec![0u32; (layout.len as usize).div_ceil(4)];
275        let g = layout.geometry;
276        region[..HEADER_WORDS].copy_from_slice(&[
277            self.seq,
278            layout.model.word(),
279            g.cols,
280            g.rows,
281            g.px_w,
282            g.px_h,
283            g.cell_px_w,
284            g.cell_px_h,
285        ]);
286        if let Backend::Host = self.backend {
287            host::surface_attach(&self.id, &as_bytes(&region)[..layout.len as usize])?;
288        }
289        if let Some(previous) = self.binding.take() {
290            // The host samples the region it had before the first resize
291            // until the next commit; an intermediate one it never read
292            // again once replaced.
293            if self.retired.is_none() {
294                self.retired = Some(previous.region);
295            }
296        }
297        self.binding = Some(Binding {
298            layout,
299            region,
300            back: 0,
301        });
302        self.full = true;
303        self.dirty.clear();
304        self.sync.clear();
305        Ok(true)
306    }
307
308    fn ensure_bound(&mut self) -> Result<&mut Binding> {
309        if self.binding.is_none() {
310            if let Some(error) = self.error.clone() {
311                return Err(error);
312            }
313            self.bind()?;
314        }
315        let id = &self.id;
316        self.binding.as_mut().ok_or_else(|| Error::NotAttached {
317            surface: id.clone(),
318        })
319    }
320
321    fn extent(&self) -> (u32, u32) {
322        self.binding
323            .as_ref()
324            .map_or((0, 0), |binding| binding.layout.extent())
325    }
326
327    /// Copies the pending rectangles from the front slot into the back.
328    fn apply_sync(&mut self) {
329        let Some(binding) = self.binding.as_mut() else {
330            self.sync.clear();
331            return;
332        };
333        if self.sync.is_empty() {
334            return;
335        }
336        let (width, _) = binding.layout.extent();
337        let unit = binding.layout.model.unit_words();
338        let slot_words = binding.layout.slot_words();
339        let front = HEADER_WORDS + usize::from(binding.back ^ 1) * slot_words;
340        let back = HEADER_WORDS + usize::from(binding.back) * slot_words;
341        for rect in self.sync.drain(..) {
342            for y in rect.y..rect.bottom() {
343                let start = (y as usize * width as usize + rect.x as usize) * unit;
344                let len = rect.w as usize * unit;
345                binding
346                    .region
347                    .copy_within(front + start..front + start + len, back + start);
348            }
349        }
350    }
351
352    /// The back slot as words, synced with the last commit, for a write
353    /// that will record its own dirty rectangles. `None` while unbound.
354    fn back(&mut self) -> Option<(&mut [u32], RegionLayout, &mut DirtyRects)> {
355        self.apply_sync();
356        let binding = self.binding.as_mut()?;
357        let slot_words = binding.layout.slot_words();
358        let start = HEADER_WORDS + usize::from(binding.back) * slot_words;
359        Some((
360            &mut binding.region[start..start + slot_words],
361            binding.layout,
362            &mut self.dirty,
363        ))
364    }
365
366    /// The back slot for a write that replaces all of it: the sync is
367    /// skipped and the whole surface is dirty.
368    fn back_discard(&mut self) -> Option<(&mut [u32], RegionLayout)> {
369        self.sync.clear();
370        let binding = self.binding.as_mut()?;
371        let (width, height) = binding.layout.extent();
372        self.dirty
373            .add(Rect::new(0, 0, width, height), width, height);
374        let slot_words = binding.layout.slot_words();
375        let start = HEADER_WORDS + usize::from(binding.back) * slot_words;
376        Some((
377            &mut binding.region[start..start + slot_words],
378            binding.layout,
379        ))
380    }
381
382    fn mark_dirty(&mut self, rect: Rect) {
383        self.apply_sync();
384        let (width, height) = self.extent();
385        self.dirty.add(rect, width, height);
386    }
387
388    fn commit(&mut self) -> Result<bool> {
389        self.ensure_bound()?;
390        let (width, height) = self.extent();
391        let full = self.full;
392        let rects = if full {
393            self.dirty.clear();
394            if width == 0 || height == 0 {
395                Vec::new()
396            } else {
397                vec![Rect::new(0, 0, width, height)]
398            }
399        } else {
400            self.dirty.take()
401        };
402        if rects.is_empty() {
403            return Ok(false);
404        }
405        let seq = self.seq.wrapping_add(1);
406        let Some(binding) = self.binding.as_mut() else {
407            return Ok(false);
408        };
409        binding.region[0] = seq;
410        let slot = binding.back;
411        if let Backend::Host = self.backend
412            && let Err(error) = host::surface_commit(&self.id, slot, &rects)
413        {
414            for rect in rects {
415                self.dirty.add(rect, width, height);
416            }
417            return Err(error);
418        }
419        binding.back ^= 1;
420        self.seq = seq;
421        self.full = false;
422        self.retired = None;
423        self.sync = rects.clone();
424        self.last_commit = Some(CommitInfo {
425            seq,
426            slot,
427            dirty: rects,
428            full,
429        });
430        Ok(true)
431    }
432}
433
434/// A surface in model `M` ([`Cells`] or [`Pixels`]). See the module docs.
435#[derive(Debug)]
436pub struct Surface<M: SurfaceModel> {
437    inner: Rc<RefCell<Inner>>,
438    model: PhantomData<M>,
439}
440
441impl<M: SurfaceModel> Surface<M> {
442    /// The manifest's surface `id`. Binds at once when the viewer has laid
443    /// it out, else on its first [`crate::Event::Resize`].
444    pub fn new(id: &str) -> Self {
445        let inner = Rc::new(RefCell::new(Inner::new(id.into(), M::MODEL, Backend::Host)));
446        registry(|registry| registry.push(Rc::downgrade(&inner)));
447        inner.borrow_mut().rebind();
448        Self {
449            inner,
450            model: PhantomData,
451        }
452    }
453
454    /// A surface of the plugin's own that the host never sees: for tests,
455    /// and to draw into a buffer before blitting it. Commits are recorded
456    /// ([`Surface::last_commit`]) but go nowhere.
457    pub fn offscreen(geometry: Geometry) -> Self {
458        let mut inner = Inner::new("offscreen".into(), M::MODEL, Backend::Offscreen(geometry));
459        inner.rebind();
460        Self {
461            inner: Rc::new(RefCell::new(inner)),
462            model: PhantomData,
463        }
464    }
465
466    /// Resizes an offscreen surface as a viewer resize would: a new
467    /// zeroed region, the next commit fully dirty. No-op for a host
468    /// surface (the host resizes those).
469    pub fn resize_offscreen(&mut self, geometry: Geometry) {
470        let mut inner = self.inner.borrow_mut();
471        if let Backend::Offscreen(current) = &mut inner.backend {
472            *current = geometry;
473            inner.rebind();
474        }
475    }
476
477    pub fn id(&self) -> String {
478        self.inner.borrow().id.clone()
479    }
480
481    /// The current geometry; `None` until the viewer lays the surface out.
482    pub fn geometry(&self) -> Option<Geometry> {
483        let inner = self.inner.borrow();
484        inner
485            .binding
486            .as_ref()
487            .map(|binding| binding.layout.geometry)
488    }
489
490    /// Width and height in the model's units: cells, or pixels.
491    pub fn size(&self) -> (u32, u32) {
492        self.inner.borrow().extent()
493    }
494
495    /// Asks the viewer for `cols` by `rows` cells (0: its choice in that
496    /// dimension) instead of the manifest's size; see
497    /// [`Context::request_size`](crate::Context::request_size).
498    pub fn request_size(&self, cols: u32, rows: u32) -> Result<()> {
499        crate::host::surface_request_size(&self.id(), cols, rows)
500    }
501
502    /// Sets the short label the viewer shows at the right end of this
503    /// surface's chrome title; see
504    /// [`Context::set_label`](crate::Context::set_label).
505    pub fn set_label(&self, label: Option<&str>) -> Result<()> {
506        crate::host::surface_set_label(&self.id(), label)
507    }
508
509    /// Places the viewer's text cursor at (`col`, `row`) of this surface
510    /// while it takes keys, or removes it; see
511    /// [`Context::set_caret`](crate::Context::set_caret).
512    pub fn set_caret(&self, caret: Option<(u32, u32)>) -> Result<()> {
513        crate::host::surface_set_caret(&self.id(), caret)
514    }
515
516    pub fn is_attached(&self) -> bool {
517        self.inner.borrow().binding.is_some()
518    }
519
520    /// Whether anything was drawn since the last commit (or the next commit
521    /// is the first at a new size).
522    pub fn is_dirty(&self) -> bool {
523        let inner = self.inner.borrow();
524        inner.binding.is_some() && (inner.full || !inner.dirty.is_empty())
525    }
526
527    /// Records a rectangle as changed, after writing through a raw slice.
528    pub fn mark_dirty(&mut self, rect: Rect) {
529        self.inner.borrow_mut().mark_dirty(rect);
530    }
531
532    /// Publishes what was drawn since the last commit and switches slots.
533    /// `Ok(false)` when nothing changed (no host call, no frame). The
534    /// first commit after a (re)bind covers the whole surface.
535    pub fn commit(&mut self) -> Result<bool> {
536        self.inner.borrow_mut().commit()
537    }
538
539    /// What the last commit published.
540    pub fn last_commit(&self) -> Option<CommitInfo> {
541        self.inner.borrow().last_commit.clone()
542    }
543
544    /// The rectangles the next commit would send (without the fully dirty
545    /// first commit's).
546    pub fn pending_dirty(&self) -> Vec<Rect> {
547        self.inner.borrow().dirty.rects().to_vec()
548    }
549
550    /// Unbinds the surface from the host. Dropping the surface does this.
551    pub fn detach(&mut self) -> Result<()> {
552        let mut inner = self.inner.borrow_mut();
553        if inner.binding.take().is_some()
554            && let Backend::Host = inner.backend
555        {
556            inner.retired = None;
557            return host::surface_detach(&inner.id);
558        }
559        Ok(())
560    }
561}
562
563impl<M: SurfaceModel> Drop for Surface<M> {
564    fn drop(&mut self) {
565        // The host must stop reading the region before its memory is
566        // freed.
567        let _ = self.detach();
568    }
569}
570
571/// A surface whose manifest lists both models (`"model": ["pixels",
572/// "cells"]`): the viewer picks one per its capabilities and may switch
573/// (graphics arriving or leaving), which the plugin sees as a
574/// [`crate::Event::Resize`]. [`AnySurface::get`] hands out the surface in
575/// the model the viewer uses now.
576#[derive(Debug)]
577pub struct AnySurface {
578    id: String,
579    surface: Option<AnySurfaceInner>,
580}
581
582#[derive(Debug)]
583enum AnySurfaceInner {
584    Cells(Surface<Cells>),
585    Pixels(Surface<Pixels>),
586}
587
588/// [`AnySurface`] in the model the viewer uses now.
589#[derive(Debug)]
590pub enum AnySurfaceMut<'a> {
591    Cells(&'a mut Surface<Cells>),
592    Pixels(&'a mut Surface<Pixels>),
593}
594
595impl AnySurface {
596    /// The manifest's surface `id`, in the model the viewer picked.
597    pub fn new(id: &str) -> Self {
598        let mut surface = Self {
599            id: id.into(),
600            surface: None,
601        };
602        surface.sync();
603        surface
604    }
605
606    pub fn id(&self) -> &str {
607        &self.id
608    }
609
610    /// The model the surface paints in now.
611    pub fn model(&self) -> Model {
612        match self.surface {
613            Some(AnySurfaceInner::Pixels(_)) => Model::Pixels,
614            _ => Model::Cells,
615        }
616    }
617
618    fn current_model(&self) -> Option<Model> {
619        let inner = match self.surface.as_ref()? {
620            AnySurfaceInner::Cells(surface) => &surface.inner,
621            AnySurfaceInner::Pixels(surface) => &surface.inner,
622        };
623        let inner = inner.borrow();
624        match &inner.error {
625            Some(Error::WrongModel { found, .. }) => Some(*found),
626            _ => Some(inner.model),
627        }
628    }
629
630    /// Follows a model switch: `true` when the surface was recreated in the
631    /// other model (it paints from scratch at its next commit).
632    pub fn sync(&mut self) -> bool {
633        let wanted = match self.current_model() {
634            Some(model) => model,
635            None => host::surface_layout(&self.id).map_or(Model::Cells, |layout| layout.model),
636        };
637        if self.surface.is_some() && Some(wanted) == self.surface.as_ref().map(|_| self.model()) {
638            return false;
639        }
640        // The old surface detaches before the new one attaches under the
641        // same id.
642        self.surface = None;
643        self.surface = Some(match wanted {
644            Model::Cells => AnySurfaceInner::Cells(Surface::new(&self.id)),
645            Model::Pixels => AnySurfaceInner::Pixels(Surface::new(&self.id)),
646        });
647        true
648    }
649
650    /// The surface in its current model, after following a switch.
651    pub fn get(&mut self) -> AnySurfaceMut<'_> {
652        self.sync();
653        match self.surface.as_mut() {
654            Some(AnySurfaceInner::Pixels(surface)) => AnySurfaceMut::Pixels(surface),
655            Some(AnySurfaceInner::Cells(surface)) => AnySurfaceMut::Cells(surface),
656            None => unreachable!("sync always leaves a surface"),
657        }
658    }
659}
660
661/// The instances of a `pane.footer` or `pane.header` surface (one per pane
662/// the viewer shows, named `<surface>@<pane>`) or of a `machine.after`
663/// surface (one per machine, `<surface>@<machine>`). Pass every event to
664/// [`Instances::event`]; it creates an instance's surface when the viewer
665/// first sizes it and drops it when the viewer removes it (a zero-size
666/// resize), and says which pane or machine changed.
667#[derive(Debug)]
668pub struct Instances<M: SurfaceModel> {
669    surface: String,
670    panes: alloc::collections::BTreeMap<String, Surface<M>>,
671}
672
673/// The pane an instance id names, when `id` is an instance of `surface`.
674pub fn instance_pane<'a>(surface: &str, id: &'a str) -> Option<&'a str> {
675    id.strip_prefix(surface)?.strip_prefix('@')
676}
677
678impl<M: SurfaceModel> Instances<M> {
679    /// The instances of the manifest's surface `surface`.
680    pub fn new(surface: &str) -> Self {
681        Self {
682            surface: surface.into(),
683            panes: alloc::collections::BTreeMap::new(),
684        }
685    }
686
687    /// Follows the viewer: returns the pane whose instance the event is
688    /// about (created, resized, shown, hidden or removed), if any.
689    pub fn event(&mut self, event: &crate::Event) -> Option<String> {
690        let (id, removed) = match event {
691            crate::Event::Resize { surface, geometry } => (surface, geometry.is_empty()),
692            crate::Event::Visibility { surface, .. } => (surface, false),
693            _ => return None,
694        };
695        let pane = String::from(instance_pane(&self.surface, id)?);
696        if removed {
697            self.panes.remove(&pane);
698        } else if !self.panes.contains_key(&pane) {
699            self.panes.insert(pane.clone(), Surface::new(id));
700        }
701        Some(pane)
702    }
703
704    pub fn get_mut(&mut self, pane: &str) -> Option<&mut Surface<M>> {
705        self.panes.get_mut(pane)
706    }
707
708    /// The instance of `owner` (a pane, machine or project id), created when
709    /// the viewer has not shown it yet: a machine or project row at zero
710    /// rows asks for rows through it
711    /// ([`Surface::request_size`]).
712    pub fn instance(&mut self, owner: &str) -> &mut Surface<M> {
713        let surface = &self.surface;
714        self.panes
715            .entry(String::from(owner))
716            .or_insert_with(|| Surface::new(&alloc::format!("{surface}@{owner}")))
717    }
718
719    /// Every live instance, by pane.
720    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut Surface<M>)> {
721        self.panes
722            .iter_mut()
723            .map(|(pane, surface)| (pane.as_str(), surface))
724    }
725
726    pub fn len(&self) -> usize {
727        self.panes.len()
728    }
729
730    pub fn is_empty(&self) -> bool {
731        self.panes.is_empty()
732    }
733}