Skip to main content

microui_redux/widgets/
display.rs

1//
2// Copyright 2022-Present (c) Raja Lehtihet & Wael El Oraiby
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are met:
6//
7// 1. Redistributions of source code must retain the above copyright notice,
8// this list of conditions and the following disclaimer.
9//
10// 2. Redistributions in binary form must reproduce the above copyright notice,
11// this list of conditions and the following disclaimer in the documentation
12// and/or other materials provided with the distribution.
13//
14// 3. Neither the name of the copyright holder nor the names of its contributors
15// may be used to endorse or promote products derived from this software without
16// specific prior written permission.
17//
18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28// POSSIBILITY OF SUCH DAMAGE.
29//
30// -----------------------------------------------------------------------------
31// Ported to rust from https://github.com/rxi/microui/ and the original license
32//
33// Copyright (c) 2020 rxi
34//
35// Permission is hereby granted, free of charge, to any person obtaining a copy
36// of this software and associated documentation files (the "Software"), to
37// deal in the Software without restriction, including without limitation the
38// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
39// sell copies of the Software, and to permit persons to whom the Software is
40// furnished to do so, subject to the following conditions:
41//
42// The above copyright notice and this permission notice shall be included in
43// all copies or substantial portions of the Software.
44//
45// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
46// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
47// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
48// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
49// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
50// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
51// IN THE SOFTWARE.
52//
53
54use crate::text_layout::build_text_lines;
55use crate::*;
56
57fn baseline_aligned_top(rect: Recti, line_height: i32, baseline: i32) -> i32 {
58    if rect.height >= line_height {
59        return rect.y + (rect.height - line_height) / 2;
60    }
61
62    let baseline_center = rect.y + rect.height / 2;
63    let min_top = rect.y + rect.height - line_height;
64    let max_top = rect.y;
65    (baseline_center - baseline).clamp(min_top, max_top)
66}
67
68fn text_lines<'a>(text: &'a str, wrap: TextWrap, max_width: i32, font: FontId, atlas: &AtlasHandle) -> Vec<crate::text_layout::TextLine> {
69    let mut lines = build_text_lines(text, wrap, max_width, font, atlas);
70    if text.ends_with('\n') {
71        if let Some(last) = lines.last() {
72            if last.start == text.len() && last.end == text.len() {
73                lines.pop();
74            }
75        }
76    }
77    lines
78}
79
80#[derive(Clone)]
81/// Non-interactive retained text block that can optionally wrap.
82pub struct TextBlock {
83    /// Text rendered by the widget.
84    pub text: String,
85    /// Wrapping mode used for layout and rendering.
86    pub wrap: TextWrap,
87    /// Font selection used for the block text.
88    pub font: FontChoice,
89    /// Widget options applied to the block.
90    pub opt: WidgetOption,
91    /// Behaviour options applied to the block.
92    pub bopt: WidgetBehaviourOption,
93}
94
95impl TextBlock {
96    /// Creates a non-interactive text block without wrapping.
97    pub fn new(text: impl Into<String>) -> Self {
98        Self::with_wrap(text, TextWrap::None)
99    }
100
101    /// Creates a non-interactive text block with an explicit wrapping mode.
102    pub fn with_wrap(text: impl Into<String>, wrap: TextWrap) -> Self {
103        Self {
104            text: text.into(),
105            wrap,
106            font: FontChoice::default(),
107            opt: WidgetOption::NO_INTERACT | WidgetOption::NO_FRAME,
108            bopt: WidgetBehaviourOption::NONE,
109        }
110    }
111
112    fn preferred_size_widget(&self, style: &Style, atlas: &AtlasHandle, avail: Dimensioni) -> Dimensioni {
113        if self.text.is_empty() {
114            return Dimensioni::new(0, 0);
115        }
116
117        let font = style.resolve_font_choice(self.font);
118        let line_height = atlas.get_font_height(font) as i32;
119        let max_width = if self.wrap == TextWrap::Word && avail.width > 0 {
120            avail.width.max(1)
121        } else {
122            i32::MAX / 4
123        };
124        let lines = text_lines(self.text.as_str(), self.wrap, max_width, font, atlas);
125        let width = lines.iter().map(|line| line.width).max().unwrap_or(0).max(0);
126        let height = line_height.saturating_mul((lines.len() as i32).max(1)).max(0);
127        Dimensioni::new(width, height)
128    }
129
130    fn handle_widget(&mut self, ctx: &mut WidgetCtx<'_>, _control: &ControlState) -> ResourceState {
131        if self.text.is_empty() {
132            return ResourceState::NONE;
133        }
134
135        let bounds = ctx.rect();
136        let font = ctx.style().resolve_font_choice(self.font);
137        let color = ctx.style().colors[ControlColor::Text as usize];
138        let line_height = ctx.atlas().get_font_height(font) as i32;
139        let baseline = ctx.atlas().get_font_baseline(font);
140        let max_width = if self.wrap == TextWrap::Word { bounds.width.max(1) } else { i32::MAX / 4 };
141        let lines = text_lines(self.text.as_str(), self.wrap, max_width, font, ctx.atlas());
142
143        ctx.push_clip_rect(bounds);
144        for (idx, line) in lines.iter().enumerate() {
145            let line_rect = rect(bounds.x, bounds.y + idx as i32 * line_height, bounds.width, line_height);
146            let line_top = baseline_aligned_top(line_rect, line_height, baseline);
147            let slice = &self.text[line.start..line.end];
148            if !slice.is_empty() {
149                ctx.draw_text(font, slice, vec2(line_rect.x, line_top), color);
150            }
151        }
152        ctx.pop_clip_rect();
153
154        ResourceState::NONE
155    }
156}
157
158implement_widget!(TextBlock, handle_widget, preferred_size_widget);
159
160#[derive(Clone)]
161/// Non-interactive filled rectangle used for retained preview swatches.
162pub struct ColorSwatch {
163    /// Fill color rendered inside the swatch.
164    pub fill: Color,
165    /// Optional label rendered on top of the swatch.
166    pub label: String,
167    /// Font selection used for the label.
168    pub font: FontChoice,
169    /// Widget options applied to the swatch.
170    pub opt: WidgetOption,
171    /// Behaviour options applied to the swatch.
172    pub bopt: WidgetBehaviourOption,
173}
174
175impl ColorSwatch {
176    /// Creates a swatch with the provided fill color.
177    pub fn new(fill: Color) -> Self {
178        Self {
179            fill,
180            label: String::new(),
181            font: FontChoice::default(),
182            opt: WidgetOption::NO_INTERACT | WidgetOption::ALIGN_CENTER,
183            bopt: WidgetBehaviourOption::NONE,
184        }
185    }
186
187    fn preferred_size_widget(&self, style: &Style, atlas: &AtlasHandle, _avail: Dimensioni) -> Dimensioni {
188        let padding = style.padding.max(0);
189        let font = style.resolve_font_choice(self.font);
190        let label_width = if self.label.is_empty() {
191            0
192        } else {
193            atlas.get_text_size(font, self.label.as_str()).width.max(0)
194        };
195        let height = (atlas.get_font_height(font) as i32 + padding * 2).max(24);
196        Dimensioni::new((label_width + padding * 2).max(24), height)
197    }
198
199    fn handle_widget(&mut self, ctx: &mut WidgetCtx<'_>, _control: &ControlState) -> ResourceState {
200        let rect = ctx.rect();
201        ctx.draw_rect(rect, self.fill);
202        let border = ctx.style().colors[ControlColor::Border as usize];
203        ctx.draw_box(rect, border);
204        if !self.label.is_empty() {
205            let font = ctx.style().resolve_font_choice(self.font);
206            ctx.draw_control_text_with_font(font, self.label.as_str(), rect, ControlColor::Text, self.opt);
207        }
208        ResourceState::NONE
209    }
210}
211
212implement_widget!(ColorSwatch, handle_widget, preferred_size_widget);