use std::cell::{Cell, RefCell};
use std::rc::Rc;
use saudade::{
App, Button, Checkbox, Color, Container, Dropdown, Event, EventCtx, Label, Painter,
PopupRequest, ProgressBar, Rect, Slider, TextInput, Theme, Widget, WindowConfig,
};
const MIN_W: i32 = 480;
const CANVAS_X: i32 = 24;
const CANVAS_Y: i32 = 198;
const MARGIN: i32 = 16;
const PANEL_PAD: i32 = 24;
const FOOTER_H: i32 = 24;
const PRESET_W: i32 = 80;
const MIN_PCT: i32 = 100;
const MAX_PCT: i32 = 300;
const PRESETS: [(&str, i32); 5] = [
("1.0x", 100),
("1.25x", 125),
("1.5x", 150),
("2.0x", 200),
("3.0x", 300),
];
const SAMPLE_W: i32 = 150;
const SAMPLE_H: i32 = 146;
fn footprint(factor: f32, zoom: bool, os_scale: f32) -> (i32, i32) {
let z = if zoom { 2.0 } else { 1.0 };
let s = os_scale.max(0.01);
let w = (SAMPLE_W as f32 * factor * z / s).round().max(1.0) as i32;
let h = (SAMPLE_H as f32 * factor * z / s).round().max(1.0) as i32;
(w, h)
}
fn window_for_footprint(fw: i32, fh: i32) -> (i32, i32) {
let w = (fw + 2 * (PANEL_PAD + CANVAS_X)).max(MIN_W);
let h = CANVAS_Y + fh + 2 * PANEL_PAD + MARGIN + FOOTER_H;
(w, h)
}
fn desired_window(factor: f32, zoom: bool, os_scale: f32) -> (i32, i32) {
let (fw, fh) = footprint(factor, zoom, os_scale);
window_for_footprint(fw, fh)
}
fn slider_rect(w: i32) -> Rect {
Rect::new(140, 92, w - 164, 22)
}
fn preset_rect(i: i32, w: i32) -> Rect {
let gap = ((w - 48 - 5 * PRESET_W) / 4).max(0);
Rect::new(24 + i * (PRESET_W + gap), 138, PRESET_W, 24)
}
fn max_tick_rect(w: i32) -> Rect {
Rect::new(w - 64, 118, 40, 14)
}
fn main() {
let factor = Rc::new(Cell::new(0.0_f32));
let zoom = Rc::new(Cell::new(false));
let os_scale = Rc::new(Cell::new(1.0_f32));
let (init_w, init_h) = window_for_footprint(SAMPLE_W, SAMPLE_H);
let resize = {
let zoom = zoom.clone();
let os_scale = os_scale.clone();
move |cx: &mut EventCtx, factor: f32| {
let (w, h) = desired_window(factor, zoom.get(), os_scale.get());
cx.request_window_size(w, h);
}
};
let slider = Rc::new(RefCell::new(
Slider::new(slider_rect(init_w), MIN_PCT, MAX_PCT)
.with_step(5)
.on_change({
let factor = factor.clone();
let resize = resize.clone();
move |cx, pct| {
let f = pct as f32 / 100.0;
factor.set(f);
resize(cx, f);
}
}),
));
let max_tick = Rc::new(RefCell::new(
Label::new(max_tick_rect(init_w), "3.0x")
.with_size(9.0)
.with_color(Color::DARK_GRAY),
));
let presets: Vec<Rc<RefCell<Button>>> = PRESETS
.iter()
.enumerate()
.map(|(i, &(label, pct))| {
let button = Button::new(preset_rect(i as i32, init_w), label).on_click({
let slider = slider.clone();
let factor = factor.clone();
let resize = resize.clone();
move |cx| {
let f = pct as f32 / 100.0;
slider.borrow_mut().set_value(pct);
factor.set(f);
resize(cx, f);
}
});
Rc::new(RefCell::new(button))
})
.collect();
let zoom_toggle = Checkbox::new(Rect::new(24, 172, init_w - 48, 16), "Zoom in 2x").on_toggle({
let zoom = zoom.clone();
let factor = factor.clone();
let os_scale = os_scale.clone();
move |cx, on| {
zoom.set(on);
let (w, h) = desired_window(factor.get().max(0.1), on, os_scale.get());
cx.request_window_size(w, h);
}
});
let mut body = Container::new(init_w, init_h)
.add(Label::new(Rect::new(24, 16, init_w - 48, 18), "Scale factor preview").with_size(13.0))
.add(
Label::new(
Rect::new(24, 38, init_w - 48, 42),
"Render a panel of widgets at any logical-to-physical scale — the\n\
window's own scale never changes, but it resizes to fit the\n\
preview. Zoom magnifies the rendered result 2x to reveal pixels.",
)
.with_size(10.0),
)
.add(FactorReadout::new(
Rect::new(24, 88, 110, 34),
factor.clone(),
zoom.clone(),
))
.add(Label::new(Rect::new(140, 118, 40, 14), "1.0x").with_size(9.0))
.add(Shared(max_tick.clone()))
.add(Shared(slider.clone()));
for preset in &presets {
body.push(Shared(preset.clone()));
}
body.push(zoom_toggle);
body.push(ScalePreview::new(
factor.clone(),
zoom.clone(),
os_scale.clone(),
));
body.push(StatusBar);
App::new(
WindowConfig::new("Scale Factor", init_w, init_h),
Root::new(body, factor.clone(), os_scale, slider, max_tick, presets),
)
.with_theme(Theme::windows_31())
.run();
}
struct Root {
inner: Container,
bounds: Rect,
factor: Rc<Cell<f32>>,
os_scale: Rc<Cell<f32>>,
slider: Rc<RefCell<Slider>>,
max_tick: Rc<RefCell<Label>>,
presets: Vec<Rc<RefCell<Button>>>,
}
impl Root {
fn new(
inner: Container,
factor: Rc<Cell<f32>>,
os_scale: Rc<Cell<f32>>,
slider: Rc<RefCell<Slider>>,
max_tick: Rc<RefCell<Label>>,
presets: Vec<Rc<RefCell<Button>>>,
) -> Self {
let (w, h) = window_for_footprint(SAMPLE_W, SAMPLE_H);
Self {
inner,
bounds: Rect::new(0, 0, w, h),
factor,
os_scale,
slider,
max_tick,
presets,
}
}
}
impl Widget for Root {
fn bounds(&self) -> Rect {
self.bounds
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
let os = painter.scale();
self.os_scale.set(os);
if self.factor.get() <= 0.0 {
self.factor.set(os);
self.slider
.borrow_mut()
.set_value((os * 100.0).round() as i32);
}
painter.fill_rect(self.bounds, Color::WHITE);
self.inner.paint(painter, theme);
}
fn paint_overlay(&mut self, painter: &mut Painter, theme: &Theme) {
self.inner.paint_overlay(painter, theme);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
self.inner.event(event, ctx);
}
fn captures_pointer(&self) -> bool {
self.inner.captures_pointer()
}
fn focusable(&self) -> bool {
self.inner.focusable()
}
fn focus_first(&mut self) -> bool {
self.inner.focus_first()
}
fn set_focused(&mut self, focused: bool) {
self.inner.set_focused(focused);
}
fn accepts_accelerators(&self) -> bool {
self.inner.accepts_accelerators()
}
fn layout(&mut self, bounds: Rect) {
self.bounds = bounds;
let w = bounds.w;
self.slider.borrow_mut().set_rect(slider_rect(w));
self.max_tick.borrow_mut().rect = max_tick_rect(w);
for (i, preset) in self.presets.iter().enumerate() {
preset.borrow_mut().rect = preset_rect(i as i32, w);
}
self.inner.layout(bounds);
}
fn popup_request(&self) -> Option<PopupRequest> {
self.inner.popup_request()
}
fn collect_popups(&self, out: &mut Vec<PopupRequest>) {
self.inner.collect_popups(out);
}
fn wants_ticks(&self) -> bool {
self.inner.wants_ticks()
}
}
struct ScalePreview {
factor: Rc<Cell<f32>>,
zoom: Rc<Cell<bool>>,
os_scale: Rc<Cell<f32>>,
sample: Vec<Box<dyn Widget>>,
}
impl ScalePreview {
fn new(factor: Rc<Cell<f32>>, zoom: Rc<Cell<bool>>, os_scale: Rc<Cell<f32>>) -> Self {
Self {
factor,
zoom,
os_scale,
sample: build_sample(),
}
}
}
fn build_sample() -> Vec<Box<dyn Widget>> {
vec![
Box::new(Label::new(Rect::new(10, 6, 130, 14), "Preview").with_size(11.0)),
Box::new(TextInput::new(Rect::new(10, 24, 130, 18)).with_text("Type here")),
Box::new(
Dropdown::new(Rect::new(10, 48, 130, 20))
.with_items(["Apple", "Banana", "Cherry"])
.with_selected(0),
),
Box::new(Checkbox::new(Rect::new(10, 76, 120, 14), "Crisp").checked(true)),
Box::new(Button::new(Rect::new(10, 98, 50, 22), "OK").default(true)),
Box::new(Button::new(Rect::new(66, 98, 56, 22), "Cancel")),
Box::new(ProgressBar::new(Rect::new(10, 128, 130, 10)).with_fraction(0.66)),
]
}
impl Widget for ScalePreview {
fn bounds(&self) -> Rect {
let (w, h) = desired_window(
self.factor.get().max(0.1),
self.zoom.get(),
self.os_scale.get(),
);
Rect::new(
CANVAS_X,
CANVAS_Y,
w - 2 * CANVAS_X,
(h - CANVAS_Y - MARGIN - FOOTER_H).max(40),
)
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
let win_scale = painter.scale().max(0.01);
let logical_w = (painter.size().w as f32 / win_scale).round() as i32;
let logical_h = (painter.size().h as f32 / win_scale).round() as i32;
let rect = Rect::new(
CANVAS_X,
CANVAS_Y,
(logical_w - 2 * CANVAS_X).max(40),
(logical_h - CANVAS_Y - MARGIN - FOOTER_H).max(40),
);
painter.fill_rect(rect, Color::WHITE);
painter.sunken_bevel(rect, theme.highlight, theme.shadow);
let factor = self.factor.get().max(0.1);
let zoom = if self.zoom.get() { 2 } else { 1 };
let content = rect.inset(2);
let fw = (SAMPLE_W as f32 * factor * zoom as f32 / win_scale)
.round()
.max(1.0) as i32;
let fh = (SAMPLE_H as f32 * factor * zoom as f32 / win_scale)
.round()
.max(1.0) as i32;
let area = Rect::new(
content.x + (content.w - fw) / 2,
content.y + (content.h - fh) / 2,
fw,
fh,
);
let saved = painter.push_clip(content);
painter.draw_scaled(area, factor, zoom, Color::WHITE, |p| {
for widget in &mut self.sample {
widget.paint(p, theme);
}
});
painter.restore_clip(saved);
}
}
struct FactorReadout {
rect: Rect,
factor: Rc<Cell<f32>>,
zoom: Rc<Cell<bool>>,
}
impl FactorReadout {
fn new(rect: Rect, factor: Rc<Cell<f32>>, zoom: Rc<Cell<bool>>) -> Self {
Self { rect, factor, zoom }
}
}
impl Widget for FactorReadout {
fn bounds(&self) -> Rect {
self.rect
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
let factor = self.factor.get();
if factor <= 0.0 {
return; }
painter.text(
self.rect.x,
self.rect.y,
&format!("{factor:.2}x"),
22.0,
theme.text,
);
let caption = if self.zoom.get() {
"preview scale, 2x zoom"
} else {
"preview scale"
};
painter.text(
self.rect.x,
self.rect.y + 24,
caption,
9.0,
theme.disabled_text,
);
}
}
struct StatusBar;
impl Widget for StatusBar {
fn bounds(&self) -> Rect {
Rect::new(0, 0, MIN_W, FOOTER_H)
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
let win_scale = painter.scale().max(0.01);
let logical_w = (painter.size().w as f32 / win_scale).round() as i32;
let logical_h = (painter.size().h as f32 / win_scale).round() as i32;
let top = logical_h - FOOTER_H;
painter.fill_rect(Rect::new(0, top, logical_w, 1), theme.shadow);
painter.fill_rect(Rect::new(0, top + 1, logical_w, 1), theme.highlight);
let system = painter.system_scale();
let mut line = format!("System scale factor: {system:.2}x");
if (win_scale - system).abs() > 0.01 {
line.push_str(&format!(
" · Resampling from {win_scale:.1}x done by compositor"
));
}
painter.text(CANVAS_X, top + 7, &line, 10.0, theme.disabled_text);
}
}
struct Shared<T>(Rc<RefCell<T>>);
impl<T: Widget> Widget for Shared<T> {
fn bounds(&self) -> Rect {
self.0.borrow().bounds()
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
self.0.borrow_mut().paint(painter, theme);
}
fn paint_overlay(&mut self, painter: &mut Painter, theme: &Theme) {
self.0.borrow_mut().paint_overlay(painter, theme);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
self.0.borrow_mut().event(event, ctx);
}
fn captures_pointer(&self) -> bool {
self.0.borrow().captures_pointer()
}
fn focusable(&self) -> bool {
self.0.borrow().focusable()
}
fn focus_first(&mut self) -> bool {
self.0.borrow_mut().focus_first()
}
fn set_focused(&mut self, focused: bool) {
self.0.borrow_mut().set_focused(focused);
}
fn accepts_accelerators(&self) -> bool {
self.0.borrow().accepts_accelerators()
}
fn layout(&mut self, bounds: Rect) {
self.0.borrow_mut().layout(bounds);
}
fn popup_request(&self) -> Option<PopupRequest> {
self.0.borrow().popup_request()
}
fn collect_popups(&self, out: &mut Vec<PopupRequest>) {
self.0.borrow().collect_popups(out);
}
fn wants_ticks(&self) -> bool {
self.0.borrow().wants_ticks()
}
}