fix: paint tall account panels on screen only

This commit is contained in:
Tomás Romero committed 2026-10-19 23:03:40 +02:00
1 parent 099d520ee2
commit d5fb0ea6e4
13 files changed
+3181 -2988

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+471 -471
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+2
View File
@@ -30,6 +30,8 @@ mod mail_view_model;
mod mail_work;
mod message_appearance;
mod oauth_browser;
#[cfg(test)]
mod on_screen_surface_tests;
pub mod pdf_preview;
pub mod platform;
mod preview_controls;
+164
View File
@@ -0,0 +1,164 @@
//! Slint's software renderer (CPU mode, Android) panics when it paints a
//! rectangle reaching past ±32767 physical pixels (#35). Surfaces that grow
//! with their content must keep painting what is on screen, and only that.
use super::*;
use slint::Rgb8Pixel;
use slint::platform::{
Platform, WindowAdapter, WindowEvent,
software_renderer::{MinimalSoftwareWindow, RepaintBufferType},
};
struct Headless(Rc<MinimalSoftwareWindow>);
impl Platform for Headless {
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, slint::PlatformError> {
Ok(self.0.clone())
}
}
slint::slint! {
import { ScrollView } from "std-widgets.slint";
import { CopyableDiagnostic } from "../ui/components/copyable-diagnostic.slint";
import { OnScreenSurface } from "../ui/components/on-screen-surface.slint";
component Panel inherits OnScreenSurface {
in property <int> rows;
background: #00ff00;
border-radius: 10px;
border-width: 1px;
border-color: #000000;
VerticalLayout {
for row in root.rows : Rectangle { height: 50px; }
}
}
export component TallSurfaces inherits Window {
in property <string> status;
in property <int> rows;
in-out property <length> content-y <=> scroll.content-y;
out property <length> status-height: status-box.height;
out property <length> panel-y: panel.y;
out property <length> panel-height: panel.height;
width: 400px;
height: 300px;
background: #ffffff;
scroll := ScrollView {
content-width: self.visible-width;
content-height: column.preferred-height;
column := VerticalLayout {
status-box := OnScreenSurface {
height: max(36px, status-text.preferred-height + 16px);
background: #ff0000;
border-radius: 8px;
border-width: 1px;
border-color: #000000;
status-text := CopyableDiagnostic {
x: 10px;
y: 8px;
width: parent.width - 20px;
text: root.status;
text-color: #000000;
font-size: 12px;
}
}
panel := Panel { rows: root.rows; }
}
}
}
}
#[test]
fn tall_surfaces_paint_only_what_is_on_screen() {
let window = MinimalSoftwareWindow::new(RepaintBufferType::NewBuffer);
slint::platform::set_platform(Box::new(Headless(window.clone()))).unwrap();
let ui = TallSurfaces::new().unwrap();
ui.window()
.dispatch_event(WindowEvent::ScaleFactorChanged { scale_factor: 2.0 });
let (width, height) = (800, 600);
ui.window()
.set_size(slint::PhysicalSize::new(width, height));
ui.set_status("A long server diagnostic\n".repeat(3_000).into());
ui.set_rows(400);
ui.show().unwrap();
let mut pixels = vec![Rgb8Pixel::default(); (width * height) as usize];
// Samples inside each surface's left padding, clear of text and border.
let mut fill_at = |content_y: f32, y: u32| {
ui.set_content_y(-content_y);
ui.window().request_redraw();
window.draw_if_needed(|r| {
r.render(&mut pixels, width as usize);
});
let pixel = pixels[(y * 2 * width + 8) as usize];
(pixel.r, pixel.g, pixel.b)
};
let red = (255, 0, 0);
let green = (0, 255, 0);
assert_eq!(fill_at(0.0, 20), red);
assert!(
ui.get_status_height() * 2.0 > 2.0 * f32::from(i16::MAX),
"the status box must exceed the renderer's range in both directions"
);
assert_eq!(fill_at(ui.get_status_height() / 2.0, 150), red);
assert!(ui.get_panel_height() * 2.0 > f32::from(i16::MAX));
let panel_middle = ui.get_panel_y() + ui.get_panel_height() / 2.0;
assert_eq!(fill_at(panel_middle, 150), green);
let end = ui.get_panel_y() + ui.get_panel_height() - 300.0;
assert_eq!(fill_at(end, 280), green);
ui.hide().unwrap();
}
#[test]
fn account_preferences_paint_hundreds_of_sender_identities() {
let window = MinimalSoftwareWindow::new(RepaintBufferType::NewBuffer);
slint::platform::set_platform(Box::new(Headless(window.clone()))).unwrap();
let app = AppWindow::new().unwrap();
app.window()
.dispatch_event(WindowEvent::ScaleFactorChanged { scale_factor: 2.0 });
app.set_startup_ready(true);
app.set_startup_hydrated(true);
app.set_connected_accounts(ModelRc::new(VecModel::from(vec![AccountRow {
id: 1,
email: "person@example.com".into(),
..Default::default()
}])));
app.set_active_view("mail".into());
app.set_settings_tab("Accounts".into());
app.set_settings_open(true);
// Servers can report hundreds of aliases for one account.
let preferences = app.global::<AccountMailPreferences>();
preferences.set_account_id(1);
preferences.set_account_label("person@example.com".into());
preferences.set_sender_identity_rows(ModelRc::new(VecModel::from(
(0..400)
.map(|index| SenderIdentityRow {
email: format!("alias{index}@example.com").into(),
primary: index == 0,
delivery: "confirmed".into(),
..Default::default()
})
.collect::<Vec<_>>(),
)));
// A tall window shows the top of the panel without scrolling.
let (width, height) = (2560, 3200);
app.window()
.set_size(slint::PhysicalSize::new(width, height));
app.show().unwrap();
let mut pixels = vec![Rgb8Pixel::default(); (width * height) as usize];
let mut paint = || {
slint::platform::update_timers_and_animations();
app.window().request_redraw();
window.draw_if_needed(|r| {
r.render(&mut pixels, width as usize);
});
};
paint();
for _ in 0..12 {
app.window().dispatch_event(WindowEvent::PointerScrolled {
position: slint::LogicalPosition::new(640.0, 800.0),
delta_x: 0.0,
delta_y: -2_000.0,
});
paint();
}
assert!(app.get_settings_open());
app.hide().unwrap();
}
+2 -1
View File
@@ -4,6 +4,7 @@ import { AppTheme } from "../design-system.slint";
import { TypeScale } from "../typography.slint";
import { Button } from "button.slint";
import { LineEdit } from "controls.slint";
import { OnScreenSurface } from "on-screen-surface.slint";
import { SettingsSelectRow, SettingsSwitchRow } from "settings-components.slint";
export struct SignatureChoice { id: string, name: string, text: string }
@@ -86,7 +87,7 @@ export global AccountMailPreferences {
in-out property <string> reader-text;
}
export component AccountMailPreferencesPanel inherits Rectangle {
export component AccountMailPreferencesPanel inherits OnScreenSurface {
in property <[AccountRow]> accounts;
in property <bool> compact: false;
in-out property <bool> security-tab: false;
+24
View File
@@ -0,0 +1,24 @@
// Slint's software renderer (CPU mode, Android) panics when it paints a
// rectangle reaching past ±32767 physical pixels (#35). A surface that grows
// with its content paints its fill and border only from just above the window
// to below the bottom of any screen, so cut edges, with their corner radius and
// border, stay out of view. Its width must follow the window.
export component OnScreenSurface {
in property <brush> background;
in property <length> border-radius;
in property <length> border-width;
in property <brush> border-color;
private property <length> paint-top: -root.absolute-position.y - 32px;
private property <length> paint-bottom: 16384phx - root.absolute-position.y;
Rectangle {
y: clamp(root.paint-top, 0px, root.height);
height: clamp(root.paint-bottom, 0px, root.height) - self.y;
background: root.background;
border-radius: root.border-radius;
border-width: root.border-width;
border-color: root.border-color;
}
@children
}
+2 -1
View File
@@ -12,6 +12,7 @@ import { ConfirmationDialog } from "../components/confirmation-dialog.slint";
import { IconButton, LineEdit, RadioButton, Switch } from "../components/controls.slint";
import { CopyableDiagnostic } from "../components/copyable-diagnostic.slint";
import { FocusVisibleScope } from "../components/focus-visible-scope.slint";
import { OnScreenSurface } from "../components/on-screen-surface.slint";
import { ProviderButton } from "../components/provider-button.slint";
import { FormField, SettingsCard, SettingsLinkRow, SettingsNavItem, SettingsSelectRow, SettingsSwitchRow, ShortcutRow } from "../components/settings-components.slint";
import { RuntimeColorPicker, ThemeEditor } from "../components/theme-editor.slint";
@@ -2687,7 +2688,7 @@ export component SettingsDialog inherits Rectangle {
}
}
if root.show_account_status && root.sync_status != "" : Rectangle {
if root.show_account_status && root.sync_status != "" : OnScreenSurface {
height: max(36px, account-status.preferred-height + 16px);
background: root.account_setup_in_progress ? root.accent-soft : AppTheme.destructive.transparentize(0.9);
border-radius: 8px;
+2 -1
View File
@@ -5,6 +5,7 @@ import { HorizontalBox, ScrollView, VerticalBox } from "std-widgets.slint";
import { Button } from "../components/button.slint";
import { CopyableDiagnostic } from "../components/copyable-diagnostic.slint";
import { LineEdit } from "../components/controls.slint";
import { OnScreenSurface } from "../components/on-screen-surface.slint";
import { ProviderButton } from "../components/provider-button.slint";
import { OAuthSetupDialog } from "../dialogs/oauth-setup-dialog.slint";
import { AppTheme } from "../design-system.slint";
@@ -369,7 +370,7 @@ export component OnboardingView inherits Rectangle {
import-certificate => { root.import-mail-certificate(); }
}
Rectangle { height: 2px; }
if root.show-account-status && root.status != "" : Rectangle {
if root.show-account-status && root.status != "" : OnScreenSurface {
height: max(36px, setup-status.preferred-height + 16px);
background: root.account-setup-in-progress ? root.hover-surface : AppTheme.destructive.transparentize(0.9);
border-radius: 8px;