xi_core_lib/
width_cache.rs

1// Copyright 2018 The xi-editor Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Cache and utilities for doing width measurement.
16
17use std::borrow::Cow;
18use std::collections::{BTreeMap, HashMap};
19
20use xi_rpc;
21
22use crate::client::Client;
23
24/// A token which can be used to retrieve an actual width value when the
25/// batch request is submitted.
26///
27/// Internally, it is implemented as an index into the `widths` array.
28pub type Token = usize;
29
30/// A measured width, in px units.
31type Width = f64;
32
33type StyleId = usize;
34
35pub struct WidthCache {
36    /// maps cache key to index within widths
37    m: HashMap<WidthCacheKey<'static>, Token>,
38    widths: Vec<Width>,
39}
40
41#[derive(Eq, PartialEq, Hash)]
42struct WidthCacheKey<'a> {
43    id: StyleId,
44    s: Cow<'a, str>,
45}
46
47/// A batched request, so that a number of strings can be measured in a
48/// a single RPC.
49pub struct WidthBatchReq<'a> {
50    cache: &'a mut WidthCache,
51    pending_tok: Token,
52    req: Vec<WidthReq>,
53    req_toks: Vec<Vec<Token>>,
54    // maps style id to index into req/req_toks
55    req_ids: BTreeMap<StyleId, Token>,
56}
57
58/// A request for measuring the widths of strings all of the same style
59/// (a request from core to front-end).
60#[derive(Serialize, Deserialize)]
61pub struct WidthReq {
62    pub id: StyleId,
63    pub strings: Vec<String>,
64}
65
66/// The response for a batch of [`WidthReq`]s.
67pub type WidthResponse = Vec<Vec<Width>>;
68
69/// A trait for types that provide width measurement. In the general case this
70/// will be provided by the frontend, but alternative implementations might
71/// be provided for faster measurement of 'fixed-width' fonts, or for testing.
72pub trait WidthMeasure {
73    fn measure_width(&self, request: &[WidthReq]) -> Result<WidthResponse, xi_rpc::Error>;
74}
75
76impl WidthMeasure for Client {
77    fn measure_width(&self, request: &[WidthReq]) -> Result<WidthResponse, xi_rpc::Error> {
78        Client::measure_width(self, request)
79    }
80}
81
82/// A measure in which each codepoint has width of 1.
83pub struct CodepointMono;
84
85impl WidthMeasure for CodepointMono {
86    /// In which each codepoint has width == 1.
87    fn measure_width(&self, request: &[WidthReq]) -> Result<WidthResponse, xi_rpc::Error> {
88        Ok(request
89            .iter()
90            .map(|r| r.strings.iter().map(|s| s.chars().count() as f64).collect())
91            .collect())
92    }
93}
94
95impl WidthCache {
96    pub fn new() -> WidthCache {
97        WidthCache { m: HashMap::new(), widths: Vec::new() }
98    }
99
100    /// Returns the number of items currently in the cache.
101    pub(crate) fn len(&self) -> usize {
102        self.m.len()
103    }
104
105    /// Resolve a previously obtained token into a width value.
106    pub fn resolve(&self, tok: Token) -> Width {
107        self.widths[tok]
108    }
109
110    /// Create a new batch of requests.
111    pub fn batch_req(self: &mut WidthCache) -> WidthBatchReq {
112        let pending_tok = self.widths.len();
113        WidthBatchReq {
114            cache: self,
115            pending_tok,
116            req: Vec::new(),
117            req_toks: Vec::new(),
118            req_ids: BTreeMap::new(),
119        }
120    }
121}
122
123impl<'a> WidthBatchReq<'a> {
124    /// Request measurement of one string/style pair within the batch.
125    pub fn request(&mut self, id: StyleId, s: &str) -> Token {
126        let key = WidthCacheKey { id, s: Cow::Borrowed(s) };
127        if let Some(tok) = self.cache.m.get(&key) {
128            return *tok;
129        }
130        // cache miss, add the request
131        let key = WidthCacheKey { id, s: Cow::Owned(s.to_owned()) };
132        let req = &mut self.req;
133        let req_toks = &mut self.req_toks;
134        let id_off = *self.req_ids.entry(id).or_insert_with(|| {
135            let id_off = req.len();
136            req.push(WidthReq { id, strings: Vec::new() });
137            req_toks.push(Vec::new());
138            id_off
139        });
140        // To avoid this second clone, we could potentially do a tricky thing where
141        // we extract the strings from the WidthReq. Probably not worth it though.
142        req[id_off].strings.push(s.to_owned());
143        let tok = self.pending_tok;
144        self.cache.m.insert(key, tok);
145        self.pending_tok += 1;
146        req_toks[id_off].push(tok);
147        tok
148    }
149
150    /// Resolves pending measurements to concrete widths using the provided [`WidthMeasure`].
151    /// On success, the tokens given by `request` will resolve in the cache.
152    pub fn resolve_pending<T: WidthMeasure + ?Sized>(
153        &mut self,
154        handler: &T,
155    ) -> Result<(), xi_rpc::Error> {
156        // The 0.0 values should all get replaced with actual widths, assuming the
157        // shape of the response from the front-end matches that of the request.
158        if self.pending_tok > self.cache.widths.len() {
159            self.cache.widths.resize(self.pending_tok, 0.0);
160            let widths = handler.measure_width(&self.req)?;
161            for (w, t) in widths.iter().zip(self.req_toks.iter()) {
162                for (width, tok) in w.iter().zip(t.iter()) {
163                    self.cache.widths[*tok] = *width;
164                }
165            }
166        }
167        Ok(())
168    }
169}