WIP

Nathan Sobo created

Change summary

crates/editor/src/display_map.rs           |   5 
crates/editor/src/display_map/block_map.rs |  28 ++
crates/editor/src/display_map/fold_map.rs  | 204 +++++++++++++++++++++--
crates/editor/src/display_map/tab_map.rs   |  35 ++-
crates/editor/src/display_map/wrap_map.rs  |  21 +
crates/editor/src/editor.rs                |  14 
crates/editor/src/multi_buffer.rs          |  45 +++--
crates/gpui/src/color.rs                   |   5 
crates/sum_tree/src/tree_map.rs            |   4 
9 files changed, 281 insertions(+), 80 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -275,7 +275,7 @@ impl DisplaySnapshot {
 
     pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
         self.blocks_snapshot
-            .chunks(display_row..self.max_point().row() + 1, false)
+            .chunks(display_row..self.max_point().row() + 1, false, None)
             .map(|h| h.text)
     }
 
@@ -284,7 +284,8 @@ impl DisplaySnapshot {
         display_rows: Range<u32>,
         language_aware: bool,
     ) -> DisplayChunks<'a> {
-        self.blocks_snapshot.chunks(display_rows, language_aware)
+        self.blocks_snapshot
+            .chunks(display_rows, language_aware, Some(&self.text_highlights))
     }
 
     pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {

crates/editor/src/display_map/block_map.rs 🔗

@@ -1,4 +1,7 @@
-use super::wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot};
+use super::{
+    wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
+    TextHighlights,
+};
 use crate::{Anchor, ToPoint as _};
 use collections::{Bound, HashMap, HashSet};
 use gpui::{AppContext, ElementBox};
@@ -555,12 +558,17 @@ impl<'a> BlockMapWriter<'a> {
 impl BlockSnapshot {
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(0..self.transforms.summary().output_rows, false)
+        self.chunks(0..self.transforms.summary().output_rows, false, None)
             .map(|chunk| chunk.text)
             .collect()
     }
 
-    pub fn chunks<'a>(&'a self, rows: Range<u32>, language_aware: bool) -> BlockChunks<'a> {
+    pub fn chunks<'a>(
+        &'a self,
+        rows: Range<u32>,
+        language_aware: bool,
+        text_highlights: Option<&'a TextHighlights>,
+    ) -> BlockChunks<'a> {
         let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
         let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
         let input_end = {
@@ -588,9 +596,11 @@ impl BlockSnapshot {
             cursor.start().1 .0 + overshoot
         };
         BlockChunks {
-            input_chunks: self
-                .wrap_snapshot
-                .chunks(input_start..input_end, language_aware),
+            input_chunks: self.wrap_snapshot.chunks(
+                input_start..input_end,
+                language_aware,
+                text_highlights,
+            ),
             input_chunk: Default::default(),
             transforms: cursor,
             output_row: rows.start,
@@ -1436,7 +1446,11 @@ mod tests {
             for start_row in 0..expected_row_count {
                 let expected_text = expected_lines[start_row..].join("\n");
                 let actual_text = blocks_snapshot
-                    .chunks(start_row as u32..blocks_snapshot.max_point().row + 1, false)
+                    .chunks(
+                        start_row as u32..blocks_snapshot.max_point().row + 1,
+                        false,
+                        None,
+                    )
                     .map(|chunk| chunk.text)
                     .collect::<String>();
                 assert_eq!(

crates/editor/src/display_map/fold_map.rs 🔗

@@ -2,16 +2,22 @@ use crate::{
     multi_buffer::MultiBufferRows, Anchor, AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot,
     ToOffset,
 };
+use collections::BTreeMap;
+use gpui::fonts::HighlightStyle;
 use language::{Chunk, Edit, Point, PointUtf16, TextSummary};
 use parking_lot::Mutex;
 use std::{
+    any::TypeId,
     cmp::{self, Ordering},
-    iter,
+    iter::{self, Peekable},
     ops::{Range, Sub},
     sync::atomic::{AtomicUsize, Ordering::SeqCst},
+    vec,
 };
 use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
 
+use super::TextHighlights;
+
 pub trait ToFoldPoint {
     fn to_fold_point(&self, snapshot: &FoldSnapshot, bias: Bias) -> FoldPoint;
 }
@@ -95,6 +101,12 @@ impl ToFoldPoint for Point {
     }
 }
 
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.output.lines;
+    }
+}
+
 pub struct FoldMapWriter<'a>(&'a mut FoldMap);
 
 impl<'a> FoldMapWriter<'a> {
@@ -500,7 +512,7 @@ impl FoldSnapshot {
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(FoldOffset(0)..self.len(), false)
+        self.chunks(FoldOffset(0)..self.len(), false, None)
             .map(|c| c.text)
             .collect()
     }
@@ -640,20 +652,96 @@ impl FoldSnapshot {
 
     pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
         let start = start.to_offset(self);
-        self.chunks(start..self.len(), false)
+        self.chunks(start..self.len(), false, None)
             .flat_map(|chunk| chunk.text.chars())
     }
 
-    pub fn chunks<'a>(&'a self, range: Range<FoldOffset>, language_aware: bool) -> FoldChunks<'a> {
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<FoldOffset>,
+        language_aware: bool,
+        text_highlights: Option<&'a TextHighlights>,
+    ) -> FoldChunks<'a> {
+        let mut highlight_endpoints = Vec::new();
         let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>();
 
-        transform_cursor.seek(&range.end, Bias::Right, &());
-        let overshoot = range.end.0 - transform_cursor.start().0 .0;
-        let buffer_end = transform_cursor.start().1 + overshoot;
+        let buffer_end = {
+            transform_cursor.seek(&range.end, Bias::Right, &());
+            let overshoot = range.end.0 - transform_cursor.start().0 .0;
+            transform_cursor.start().1 + overshoot
+        };
 
-        transform_cursor.seek(&range.start, Bias::Right, &());
-        let overshoot = range.start.0 - transform_cursor.start().0 .0;
-        let buffer_start = transform_cursor.start().1 + overshoot;
+        let buffer_start = {
+            transform_cursor.seek(&range.start, Bias::Right, &());
+            let overshoot = range.start.0 - transform_cursor.start().0 .0;
+            transform_cursor.start().1 + overshoot
+        };
+
+        if let Some(text_highlights) = text_highlights {
+            if !text_highlights.is_empty() {
+                while transform_cursor.start().0 < range.end {
+                    if !transform_cursor.item().unwrap().is_fold() {
+                        let transform_start = self
+                            .buffer_snapshot
+                            .anchor_after(cmp::max(buffer_start, transform_cursor.start().1));
+
+                        let transform_end = {
+                            let overshoot = range.end.0 - transform_cursor.start().0 .0;
+                            self.buffer_snapshot.anchor_before(cmp::min(
+                                transform_cursor.end(&()).1,
+                                transform_cursor.start().1 + overshoot,
+                            ))
+                        };
+
+                        for (tag, highlights) in text_highlights.iter() {
+                            let style = highlights.0;
+                            let ranges = &highlights.1;
+
+                            let start_ix = match ranges.binary_search_by(|probe| {
+                                let cmp = probe
+                                    .end
+                                    .cmp(&transform_start, &self.buffer_snapshot())
+                                    .unwrap();
+                                if cmp.is_gt() {
+                                    Ordering::Greater
+                                } else {
+                                    Ordering::Less
+                                }
+                            }) {
+                                Ok(i) | Err(i) => i,
+                            };
+                            for range in &ranges[start_ix..] {
+                                if range
+                                    .start
+                                    .cmp(&transform_end, &self.buffer_snapshot)
+                                    .unwrap()
+                                    .is_ge()
+                                {
+                                    break;
+                                }
+
+                                highlight_endpoints.push(HighlightEndpoint {
+                                    offset: range.start.to_offset(&self.buffer_snapshot),
+                                    is_start: true,
+                                    tag: *tag,
+                                    style,
+                                });
+                                highlight_endpoints.push(HighlightEndpoint {
+                                    offset: range.end.to_offset(&self.buffer_snapshot),
+                                    is_start: false,
+                                    tag: *tag,
+                                    style,
+                                });
+                            }
+                        }
+                    }
+
+                    transform_cursor.next(&());
+                }
+                highlight_endpoints.sort();
+                transform_cursor.seek(&range.start, Bias::Right, &());
+            }
+        }
 
         FoldChunks {
             transform_cursor,
@@ -664,6 +752,8 @@ impl FoldSnapshot {
             buffer_offset: buffer_start,
             output_offset: range.start.0,
             max_output_offset: range.end.0,
+            highlight_endpoints: highlight_endpoints.into_iter().peekable(),
+            active_highlights: Default::default(),
         }
     }
 
@@ -952,6 +1042,8 @@ pub struct FoldChunks<'a> {
     buffer_offset: usize,
     output_offset: usize,
     max_output_offset: usize,
+    highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
+    active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
 }
 
 impl<'a> Iterator for FoldChunks<'a> {
@@ -990,6 +1082,21 @@ impl<'a> Iterator for FoldChunks<'a> {
             });
         }
 
+        let mut next_highlight_endpoint = usize::MAX;
+        while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
+            if endpoint.offset <= self.buffer_offset {
+                if endpoint.is_start {
+                    self.active_highlights.insert(endpoint.tag, endpoint.style);
+                } else {
+                    self.active_highlights.remove(&endpoint.tag);
+                }
+                self.highlight_endpoints.next();
+            } else {
+                next_highlight_endpoint = endpoint.offset;
+                break;
+            }
+        }
+
         // Retrieve a chunk from the current location in the buffer.
         if self.buffer_chunk.is_none() {
             let chunk_offset = self.buffer_chunks.offset();
@@ -997,20 +1104,31 @@ impl<'a> Iterator for FoldChunks<'a> {
         }
 
         // Otherwise, take a chunk from the buffer's text.
-        if let Some((chunk_offset, mut chunk)) = self.buffer_chunk {
-            let offset_in_chunk = self.buffer_offset - chunk_offset;
-            chunk.text = &chunk.text[offset_in_chunk..];
-
-            // Truncate the chunk so that it ends at the next fold.
-            let region_end = self.transform_cursor.end(&()).1 - self.buffer_offset;
-            if chunk.text.len() >= region_end {
-                chunk.text = &chunk.text[0..region_end];
+        if let Some((buffer_chunk_start, mut chunk)) = self.buffer_chunk {
+            let buffer_chunk_end = buffer_chunk_start + chunk.text.len();
+            let transform_end = self.transform_cursor.end(&()).1;
+            let chunk_end = buffer_chunk_end
+                .min(transform_end)
+                .min(next_highlight_endpoint);
+
+            chunk.text = &chunk.text
+                [self.buffer_offset - buffer_chunk_start..chunk_end - buffer_chunk_start];
+
+            if !self.active_highlights.is_empty() {
+                let mut highlight_style = HighlightStyle::default();
+                for active_highlight in self.active_highlights.values() {
+                    highlight_style.highlight(*active_highlight);
+                }
+                chunk.highlight_style = Some(highlight_style);
+            }
+
+            if chunk_end == transform_end {
                 self.transform_cursor.next(&());
-            } else {
+            } else if chunk_end == buffer_chunk_end {
                 self.buffer_chunk.take();
             }
 
-            self.buffer_offset += chunk.text.len();
+            self.buffer_offset = chunk_end;
             self.output_offset += chunk.text.len();
             return Some(chunk);
         }
@@ -1019,9 +1137,25 @@ impl<'a> Iterator for FoldChunks<'a> {
     }
 }
 
-impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint {
-    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
-        self.0 += &summary.output.lines;
+#[derive(Copy, Clone, Eq, PartialEq)]
+struct HighlightEndpoint {
+    offset: usize,
+    is_start: bool,
+    tag: Option<TypeId>,
+    style: HighlightStyle,
+}
+
+impl PartialOrd for HighlightEndpoint {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for HighlightEndpoint {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.offset
+            .cmp(&other.offset)
+            .then_with(|| self.is_start.cmp(&other.is_start))
     }
 }
 
@@ -1078,7 +1212,8 @@ mod tests {
     use super::*;
     use crate::{MultiBuffer, ToPoint};
     use rand::prelude::*;
-    use std::{env, mem};
+    use std::{cmp::Reverse, env, mem, sync::Arc};
+    use sum_tree::TreeMap;
     use text::RandomCharIter;
     use util::test::sample_text;
     use Bias::{Left, Right};
@@ -1283,6 +1418,25 @@ mod tests {
         let (mut initial_snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
         let mut snapshot_edits = Vec::new();
 
+        let mut highlights = TreeMap::default();
+        let highlight_count = rng.gen_range(0_usize..10);
+        let mut highlight_ranges = (0..highlight_count)
+            .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
+            .collect::<Vec<_>>();
+        highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
+        log::info!("highlighting ranges {:?}", highlight_ranges);
+        let highlight_ranges = highlight_ranges
+            .into_iter()
+            .map(|range| {
+                buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end)
+            })
+            .collect::<Vec<_>>();
+
+        highlights.insert(
+            Some(TypeId::of::<()>()),
+            Arc::new((HighlightStyle::default(), highlight_ranges)),
+        );
+
         for _ in 0..operations {
             log::info!("text: {:?}", buffer_snapshot.text());
             let mut buffer_edits = Vec::new();
@@ -1407,7 +1561,7 @@ mod tests {
                 let text = &expected_text[start.0..end.0];
                 assert_eq!(
                     snapshot
-                        .chunks(start..end, false)
+                        .chunks(start..end, false, Some(&highlights))
                         .map(|c| c.text)
                         .collect::<String>(),
                     text,

crates/editor/src/display_map/tab_map.rs 🔗

@@ -1,4 +1,7 @@
-use super::fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot, ToFoldPoint};
+use super::{
+    fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot, ToFoldPoint},
+    TextHighlights,
+};
 use crate::MultiBufferSnapshot;
 use language::{rope, Chunk};
 use parking_lot::Mutex;
@@ -32,9 +35,10 @@ impl TabMap {
         let mut tab_edits = Vec::with_capacity(fold_edits.len());
         for fold_edit in &mut fold_edits {
             let mut delta = 0;
-            for chunk in old_snapshot
-                .fold_snapshot
-                .chunks(fold_edit.old.end..max_offset, false)
+            for chunk in
+                old_snapshot
+                    .fold_snapshot
+                    .chunks(fold_edit.old.end..max_offset, false, None)
             {
                 let patterns: &[_] = &['\t', '\n'];
                 if let Some(ix) = chunk.text.find(patterns) {
@@ -109,7 +113,7 @@ impl TabSnapshot {
             self.max_point()
         };
         for c in self
-            .chunks(range.start..line_end, false)
+            .chunks(range.start..line_end, false, None)
             .flat_map(|chunk| chunk.text.chars())
         {
             if c == '\n' {
@@ -123,7 +127,7 @@ impl TabSnapshot {
             last_line_chars = first_line_chars;
         } else {
             for _ in self
-                .chunks(TabPoint::new(range.end.row(), 0)..range.end, false)
+                .chunks(TabPoint::new(range.end.row(), 0)..range.end, false, None)
                 .flat_map(|chunk| chunk.text.chars())
             {
                 last_line_chars += 1;
@@ -143,7 +147,12 @@ impl TabSnapshot {
         self.fold_snapshot.version
     }
 
-    pub fn chunks<'a>(&'a self, range: Range<TabPoint>, language_aware: bool) -> TabChunks<'a> {
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<TabPoint>,
+        language_aware: bool,
+        text_highlights: Option<&'a TextHighlights>,
+    ) -> TabChunks<'a> {
         let (input_start, expanded_char_column, to_next_stop) =
             self.to_fold_point(range.start, Bias::Left);
         let input_start = input_start.to_offset(&self.fold_snapshot);
@@ -158,9 +167,11 @@ impl TabSnapshot {
         };
 
         TabChunks {
-            fold_chunks: self
-                .fold_snapshot
-                .chunks(input_start..input_end, language_aware),
+            fold_chunks: self.fold_snapshot.chunks(
+                input_start..input_end,
+                language_aware,
+                text_highlights,
+            ),
             column: expanded_char_column,
             output_position: range.start.0,
             max_output_position: range.end.0,
@@ -179,7 +190,7 @@ impl TabSnapshot {
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(TabPoint::zero()..self.max_point(), false)
+        self.chunks(TabPoint::zero()..self.max_point(), false, None)
             .map(|chunk| chunk.text)
             .collect()
     }
@@ -492,7 +503,7 @@ mod tests {
             assert_eq!(
                 expected_text,
                 tabs_snapshot
-                    .chunks(start..end, false)
+                    .chunks(start..end, false, None)
                     .map(|c| c.text)
                     .collect::<String>(),
                 "chunks({:?}..{:?})",

crates/editor/src/display_map/wrap_map.rs 🔗

@@ -1,6 +1,7 @@
 use super::{
     fold_map,
     tab_map::{self, TabEdit, TabPoint, TabSnapshot},
+    TextHighlights,
 };
 use crate::{MultiBufferSnapshot, Point};
 use gpui::{
@@ -433,6 +434,7 @@ impl WrapSnapshot {
                 let mut chunks = new_tab_snapshot.chunks(
                     TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
                     false,
+                    None,
                 );
                 let mut edit_transforms = Vec::<Transform>::new();
                 for _ in edit.new_rows.start..edit.new_rows.end {
@@ -558,11 +560,16 @@ impl WrapSnapshot {
     }
 
     pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
-        self.chunks(wrap_row..self.max_point().row() + 1, false)
+        self.chunks(wrap_row..self.max_point().row() + 1, false, None)
             .map(|h| h.text)
     }
 
-    pub fn chunks<'a>(&'a self, rows: Range<u32>, language_aware: bool) -> WrapChunks<'a> {
+    pub fn chunks<'a>(
+        &'a self,
+        rows: Range<u32>,
+        language_aware: bool,
+        text_highlights: Option<&'a TextHighlights>,
+    ) -> WrapChunks<'a> {
         let output_start = WrapPoint::new(rows.start, 0);
         let output_end = WrapPoint::new(rows.end, 0);
         let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>();
@@ -575,9 +582,11 @@ impl WrapSnapshot {
             .to_tab_point(output_end)
             .min(self.tab_snapshot.max_point());
         WrapChunks {
-            input_chunks: self
-                .tab_snapshot
-                .chunks(input_start..input_end, language_aware),
+            input_chunks: self.tab_snapshot.chunks(
+                input_start..input_end,
+                language_aware,
+                text_highlights,
+            ),
             input_chunk: Default::default(),
             output_position: output_start,
             max_output_row: rows.end,
@@ -1280,7 +1289,7 @@ mod tests {
                 }
 
                 let actual_text = self
-                    .chunks(start_row..end_row, true)
+                    .chunks(start_row..end_row, true, None)
                     .map(|c| c.text)
                     .collect::<String>();
                 assert_eq!(

crates/editor/src/editor.rs 🔗

@@ -4398,16 +4398,16 @@ impl Editor {
                             None,
                             cx,
                         );
-                        editor.highlight_background::<Rename>(
-                            vec![Anchor::min()..Anchor::max()],
-                            style.diff_background_inserted,
-                            cx,
-                        );
                         editor
                     });
-                    this.highlight_background::<Rename>(
+                    this.highlight_text::<Rename>(
                         vec![range.clone()],
-                        style.diff_background_deleted,
+                        HighlightStyle {
+                            color: Color::transparent_black(),
+                            font_properties: todo!(),
+                            underline: todo!(),
+                            fade_out: todo!(),
+                        },
                         cx,
                     );
                     this.update_selections(

crates/editor/src/multi_buffer.rs 🔗

@@ -213,25 +213,6 @@ impl MultiBuffer {
         this
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn build_simple(text: &str, cx: &mut gpui::MutableAppContext) -> ModelHandle<Self> {
-        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
-        cx.add_model(|cx| Self::singleton(buffer, cx))
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn build_random(
-        rng: &mut impl rand::Rng,
-        cx: &mut gpui::MutableAppContext,
-    ) -> ModelHandle<Self> {
-        cx.add_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(0);
-            let mutation_count = rng.gen_range(1..=5);
-            multibuffer.randomly_edit_excerpts(rng, mutation_count, cx);
-            multibuffer
-        })
-    }
-
     pub fn replica_id(&self) -> ReplicaId {
         self.replica_id
     }
@@ -1170,6 +1151,23 @@ impl MultiBuffer {
 
 #[cfg(any(test, feature = "test-support"))]
 impl MultiBuffer {
+    pub fn build_simple(text: &str, cx: &mut gpui::MutableAppContext) -> ModelHandle<Self> {
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
+        cx.add_model(|cx| Self::singleton(buffer, cx))
+    }
+
+    pub fn build_random(
+        rng: &mut impl rand::Rng,
+        cx: &mut gpui::MutableAppContext,
+    ) -> ModelHandle<Self> {
+        cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(0);
+            let mutation_count = rng.gen_range(1..=5);
+            multibuffer.randomly_edit_excerpts(rng, mutation_count, cx);
+            multibuffer
+        })
+    }
+
     pub fn randomly_edit(
         &mut self,
         rng: &mut impl rand::Rng,
@@ -2300,6 +2298,15 @@ impl MultiBufferSnapshot {
     }
 }
 
+#[cfg(any(test, feature = "test-support"))]
+impl MultiBufferSnapshot {
+    pub fn random_byte_range(&self, start_offset: usize, rng: &mut impl rand::Rng) -> Range<usize> {
+        let end = self.clip_offset(rng.gen_range(start_offset..=self.len()), Bias::Right);
+        let start = self.clip_offset(rng.gen_range(start_offset..=end), Bias::Right);
+        start..end
+    }
+}
+
 impl History {
     fn start_transaction(&mut self, now: Instant) -> Option<TransactionId> {
         self.transaction_depth += 1;

crates/gpui/src/color.rs 🔗

@@ -50,8 +50,9 @@ impl Color {
     }
 
     pub fn blend(source: Color, dest: Color) -> Color {
-        if dest.a == 255 {
-            return dest;
+        // If source is fully opaque, don't blend.
+        if source.a == 255 {
+            return source;
         }
 
         let source = source.0.to_f32();

crates/sum_tree/src/tree_map.rs 🔗

@@ -31,6 +31,10 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
         Self(tree)
     }
 
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+
     pub fn get<'a>(&self, key: &'a K) -> Option<&V> {
         let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
         cursor.seek(&MapKeyRef(Some(key)), Bias::Left, &());