mirror of
https://github.com/flectar/mail.git
synced 2026-11-14 10:54:50 +00:00
fix: paint tall account panels on screen only
This commit is contained in:
1 parent
099d520ee2
commit
d5fb0ea6e4
13 files changed
+3181
-2988
No files matched your search
+419
-419
File diff suppressed because it is too large.
Load diff
+419
-419
File diff suppressed because it is too large.
Load diff
+471
-471
File diff suppressed because it is too large.
Load diff
+419
-419
File diff suppressed because it is too large.
Load diff
+419
-419
File diff suppressed because it is too large.
Load diff
+419
-419
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user