Skip to content

Commit e4bf75b

Browse files
committed
Merge with master
2 parents a4278e6 + 3b15d18 commit e4bf75b

File tree

69 files changed

+376
-97
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+376
-97
lines changed

.github/workflows/autofix.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- uses: actions/checkout@v5
3030
- uses: jdx/mise-action@v3
3131
with:
32-
version: 2025.7.12
32+
version: 2025.9.18
3333
log_level: debug
3434
- name: Run fixes
3535
run: mise run --force --jobs=1 'ci:autofix:fix'
@@ -43,7 +43,7 @@ jobs:
4343
- uses: actions/checkout@v5
4444
- uses: jdx/mise-action@v3
4545
with:
46-
version: 2025.7.12
46+
version: 2025.9.18
4747
log_level: debug
4848
- name: Run lints
4949
run: mise run --force --jobs=1 'ci:autofix:lint'

.github/workflows/material.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888
with:
8989
version: 10.14.0
9090
- name: Take screenshots
91-
run: cargo run -p slint-docsnapper -- -I$PWD/src docs
91+
run: cargo run -p slint-docsnapper -- -Lmaterial=$PWD/src/material.slint docs
9292
working-directory: ui-libraries/material
9393
- name: Install dependencies
9494
run: pnpm install --frozen-lockfile
@@ -102,6 +102,15 @@ jobs:
102102
with:
103103
name: apk_gallery
104104
path: ui-libraries/material/docs/dist/apk
105+
- name: prepare zip archive
106+
working-directory: ui-libraries/material
107+
run: |
108+
cp -a src material-1.0
109+
- name: zip up material library
110+
working-directory: ui-libraries/material
111+
run: |
112+
mkdir -p docs/dist/zip
113+
zip -r docs/dist/zip/material-1.0.zip material-1.0
105114
- name: Deploy
106115
uses: cloudflare/wrangler-action@v3
107116
with:

api/python/slint/timer.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ impl From<PyTimerMode> for i_slint_core::timers::TimerMode {
3737
/// The timer will automatically stop when garbage collected. You must keep the Timer object
3838
/// around for as long as you want the timer to keep firing.
3939
///
40+
/// ```python
41+
/// class AppWindow(...)
42+
/// def __init__(self):
43+
/// super().__init__()
44+
/// self.my_timer = None
45+
///
46+
/// @slint.callback
47+
/// def button_clicked(self):
48+
/// self.my_timer = slint.Timer()
49+
/// self.my_timer.start(timedelta(seconds=1), self.do_something)
50+
///
51+
/// def do_something(self):
52+
/// pass
53+
/// ```
54+
///
4055
/// Timers can only be used in the thread that runs the Slint event loop. They don't
4156
/// fire if used in another thread.
4257
#[gen_stub_pyclass]

internal/compiler/passes/resolving.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use smol_str::{SmolStr, ToSmolStr};
2020
use std::collections::{BTreeMap, HashMap};
2121
use std::rc::Rc;
2222

23+
mod remove_noop;
24+
2325
/// This represents a scope for the Component, where Component is the repeated component, but
2426
/// does not represent a component in the .slint file
2527
#[derive(Clone)]
@@ -211,13 +213,26 @@ impl Expression {
211213
let mut statements_or_exprs = node
212214
.children()
213215
.filter_map(|n| match n.kind() {
214-
SyntaxKind::Expression => Some(Self::from_expression_node(n.into(), ctx)),
215-
SyntaxKind::ReturnStatement => Some(Self::from_return_statement(n.into(), ctx)),
216-
SyntaxKind::LetStatement => Some(Self::from_let_statement(n.into(), ctx)),
216+
SyntaxKind::Expression => {
217+
Some((n.clone(), Self::from_expression_node(n.into(), ctx)))
218+
}
219+
SyntaxKind::ReturnStatement => {
220+
Some((n.clone(), Self::from_return_statement(n.into(), ctx)))
221+
}
222+
SyntaxKind::LetStatement => {
223+
Some((n.clone(), Self::from_let_statement(n.into(), ctx)))
224+
}
217225
_ => None,
218226
})
219227
.collect::<Vec<_>>();
220228

229+
remove_noop::remove_from_codeblock(&mut statements_or_exprs, ctx.diag);
230+
231+
let mut statements_or_exprs = statements_or_exprs
232+
.into_iter()
233+
.map(|(_node, statement_or_expr)| statement_or_expr)
234+
.collect::<Vec<_>>();
235+
221236
let exit_points_and_return_types = statements_or_exprs
222237
.iter()
223238
.enumerate()
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright © SixtyFPS GmbH <[email protected]>
2+
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3+
4+
use crate::{diagnostics::BuildDiagnostics, expression_tree::Expression, parser::SyntaxNode};
5+
6+
/// Remove all expressions that are proven to have no effect from the given Expressions.
7+
///
8+
/// This function assumes that the given Expressions will form the an [Expression::CodeBlock], and
9+
/// will therefore not modify the last Expression in the Vec, as that forms the result value of the
10+
/// CodeBlock itself.
11+
pub fn remove_from_codeblock(
12+
code_block: &mut Vec<(SyntaxNode, Expression)>,
13+
diagnostics: &mut BuildDiagnostics,
14+
) {
15+
if code_block.len() > 1 {
16+
// In a code block, only the last expression returns a value.
17+
// Therefore all other expressions inside the block are only useful if they have side
18+
// effects.
19+
//
20+
// Remove all expressions without side effects (except for the last one) and emit a
21+
// warning.
22+
//
23+
// Note: Iterate over the indices in reverse, so that all to-be-iterated indices remain
24+
// valid when removing items from the vector.
25+
for index in (0..(code_block.len() - 1)).rev() {
26+
let (node, expression) = &code_block[index];
27+
if without_side_effects(expression) {
28+
diagnostics.push_warning("Expression has no effect!".to_owned(), node);
29+
code_block.remove(index);
30+
}
31+
}
32+
}
33+
}
34+
35+
/// Returns whether the expression is certain to be without side effects.
36+
/// This function is conservative and may still return `false`, even if a given expression
37+
/// is without side effects.
38+
/// It is only guaranteed that if this function returns `true`, the expression definitely does not
39+
/// contain side effects.
40+
fn without_side_effects(expression: &Expression) -> bool {
41+
match expression {
42+
Expression::Condition { condition, true_expr, false_expr } => {
43+
without_side_effects(condition)
44+
&& without_side_effects(true_expr)
45+
&& without_side_effects(false_expr)
46+
}
47+
Expression::NumberLiteral(_, _) => true,
48+
Expression::StringLiteral(_) => true,
49+
Expression::BoolLiteral(_) => true,
50+
Expression::CodeBlock(expressions) => expressions.iter().all(without_side_effects),
51+
Expression::FunctionParameterReference { .. } => true,
52+
// Invalid and uncompiled expressions are unknown at this point, so default to
53+
// `false`, because they may have side-efffects.
54+
Expression::Invalid => false,
55+
Expression::Uncompiled(_) => false,
56+
// A property reference may cause re-evaluation of a property, which may result in
57+
// side effects
58+
Expression::PropertyReference(_) => false,
59+
Expression::ElementReference(_) => false,
60+
Expression::RepeaterIndexReference { .. } => true,
61+
Expression::RepeaterModelReference { .. } => true,
62+
Expression::StoreLocalVariable { .. } => false,
63+
Expression::ReadLocalVariable { .. } => true,
64+
Expression::StructFieldAccess { base, name: _ } => without_side_effects(&*base),
65+
Expression::ArrayIndex { array, index } => {
66+
without_side_effects(&*array) && without_side_effects(&*index)
67+
}
68+
// Note: This assumes that the cast itself does not have any side effects, which may not be
69+
// the case if custom casting rules are implemented.
70+
Expression::Cast { from, to: _ } => without_side_effects(from),
71+
// Note: Calling a *pure* function is without side effects, however
72+
// just from the expression, the purity of the function is not known.
73+
// We would need to resolve the function to determine its purity.
74+
Expression::FunctionCall { .. } => false,
75+
Expression::SelfAssignment { .. } => false,
76+
Expression::BinaryExpression { lhs, rhs, .. } => {
77+
without_side_effects(&*lhs) && without_side_effects(&*rhs)
78+
}
79+
Expression::UnaryOp { sub, op: _ } => without_side_effects(&*sub),
80+
Expression::ImageReference { .. } => true,
81+
Expression::Array { element_ty: _, values } => values.iter().all(without_side_effects),
82+
Expression::Struct { ty: _, values } => values.values().all(without_side_effects),
83+
Expression::PathData(_) => true,
84+
Expression::EasingCurve(_) => true,
85+
Expression::LinearGradient { angle, stops } => {
86+
without_side_effects(&angle)
87+
&& stops
88+
.iter()
89+
.all(|(start, end)| without_side_effects(start) && without_side_effects(end))
90+
}
91+
Expression::RadialGradient { stops } => stops
92+
.iter()
93+
.all(|(start, end)| without_side_effects(start) && without_side_effects(end)),
94+
Expression::ConicGradient { stops } => stops
95+
.iter()
96+
.all(|(start, end)| without_side_effects(start) && without_side_effects(end)),
97+
Expression::EnumerationValue(_) => true,
98+
// A return statement is never without side effects, as an important "side effect" is that
99+
// the current function stops at this point.
100+
Expression::ReturnStatement(_) => false,
101+
Expression::LayoutCacheAccess { .. } => false,
102+
Expression::ComputeLayoutInfo(_, _) => false,
103+
Expression::SolveLayout(_, _) => false,
104+
Expression::MinMax { ty: _, op: _, lhs, rhs } => {
105+
without_side_effects(lhs) && without_side_effects(rhs)
106+
}
107+
Expression::DebugHook { .. } => false,
108+
Expression::EmptyComponentFactory => false,
109+
}
110+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright © SixtyFPS GmbH <[email protected]>
2+
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3+
4+
struct MyStruct {
5+
foo: int
6+
}
7+
8+
enum MyEnum {
9+
One,
10+
Two,
11+
Three
12+
}
13+
14+
export component Test {
15+
function useless_if(cond: bool) {
16+
if (cond) {
17+
// ^warning{Expression has no effect!}
18+
if (!cond) {
19+
// ^warning{Expression has no effect!}
20+
43
21+
}
22+
42
23+
}
24+
else {
25+
41
26+
}
27+
"hello world";
28+
// ^warning{Expression has no effect!}
29+
123;
30+
// ^warning{Expression has no effect!}
31+
true;
32+
// ^warning{Expression has no effect!}
33+
MyEnum.One;
34+
// ^warning{Expression has no effect!}
35+
{ x: 32, };
36+
// ^warning{Expression has no effect!}
37+
{ x: another_function(), };
38+
39+
let x = true;
40+
x;
41+
// ^warning{Expression has no effect!}
42+
true && false;
43+
// ^warning{Expression has no effect!}
44+
[1, 2, 3][4];
45+
// ^warning{Expression has no effect!}
46+
+1;
47+
// ^warning{Expression has no effect!}
48+
@image-url("../../../../../logo/slint-logo-full-dark.png");
49+
// ^warning{Expression has no effect!}
50+
51+
another_function();
52+
53+
[1, 2, 3][another_function()];
54+
// FIXME: The above could return an "Expression has no effect" warning
55+
// on the [1, 2, 3] part, as well as the indexing, as only the call
56+
// to another_function could have an effect.
57+
58+
make_struct().foo;
59+
// FIXME: The above could return an "Expression has no effect" warning
60+
// on the .foo part, as only the call to make_struct() could have an effect,
61+
// not the member access itself.
62+
63+
12
64+
}
65+
66+
function another_function() -> int { 1 }
67+
68+
function make_struct() -> MyStruct {
69+
{
70+
foo: 5,
71+
}
72+
}
73+
74+
}

internal/core/animations.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,9 @@ impl AnimationDriver {
249249
/// Iterates through all animations based on the new time tick and updates their state. This should be called by
250250
/// the windowing system driver for every frame.
251251
pub fn update_animations(&self, new_tick: Instant) {
252-
if self.global_instant.as_ref().get_untracked() != new_tick {
252+
let current_tick = self.global_instant.as_ref().get_untracked();
253+
assert!(current_tick <= new_tick, "The platform's clock is not monotonic!");
254+
if current_tick != new_tick {
253255
self.active_animations.set(false);
254256
self.global_instant.as_ref().set(new_tick);
255257
}

internal/core/layout.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -624,10 +624,16 @@ pub fn box_layout_info(
624624
alignment: LayoutAlignment,
625625
) -> LayoutInfo {
626626
let count = cells.len();
627+
let is_stretch = alignment == LayoutAlignment::Stretch;
627628
if count < 1 {
628-
return LayoutInfo { max: 0 as _, ..LayoutInfo::default() };
629+
let mut info = LayoutInfo::default();
630+
info.min = padding.begin + padding.end;
631+
info.preferred = info.min;
632+
if is_stretch {
633+
info.max = info.min;
634+
}
635+
return info;
629636
};
630-
let is_stretch = alignment == LayoutAlignment::Stretch;
631637
let extra_w = padding.begin + padding.end + spacing * (count - 1) as Coord;
632638
let min = cells.iter().map(|c| c.constraint.min).sum::<Coord>() + extra_w;
633639
let max = if is_stretch {
@@ -641,12 +647,7 @@ pub fn box_layout_info(
641647
}
642648

643649
pub fn box_layout_info_ortho(cells: Slice<BoxLayoutCellData>, padding: &Padding) -> LayoutInfo {
644-
let count = cells.len();
645-
if count < 1 {
646-
return LayoutInfo { max: 0 as _, ..LayoutInfo::default() };
647-
};
648650
let extra_w = padding.begin + padding.end;
649-
650651
let mut fold =
651652
cells.iter().fold(LayoutInfo { stretch: f32::MAX, ..Default::default() }, |a, b| {
652653
a.merge(&b.constraint)

internal/core/platform.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ pub trait Platform {
9494
#[cfg(feature = "std")]
9595
{
9696
let the_beginning = *INITIAL_INSTANT.get_or_init(time::Instant::now);
97-
time::Instant::now() - the_beginning
97+
let now = time::Instant::now();
98+
assert!(now >= the_beginning, "The platform's clock is not monotonic!");
99+
now - the_beginning
98100
}
99101
#[cfg(not(feature = "std"))]
100102
unimplemented!("The platform abstraction must implement `duration_since_start`")

tests/cases/crashes/layout_deleted_item.slint

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export TestCase := Window {
1313
property<[int]> model: [1];
1414
VerticalLayout {
1515
under := TouchArea {
16-
HorizontalLayout {
16+
VerticalLayout {
1717
for value in model: TouchArea {
1818
horizontal-stretch: 5;
1919
vertical-stretch: 5;

0 commit comments

Comments
 (0)