ltk/event_loop/text_editing/
ime.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use smithay_client_toolkit::reexports::client::QueueHandle;
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3;

use crate::app::App;
use crate::event_loop::app_data::AppData;
use crate::event_loop::surface::SurfaceFocus;
use crate::tree::find_widget;
use crate::types::Rect;
use crate::widget::WidgetHandlers;

impl<A: App> AppData<A>
{
	pub( crate ) fn activate_text_input( &mut self, qh: &QueueHandle<Self>, secure: bool )
	{
		self.text_input_secure = secure;
		let ( hint, purpose ) = content_type( secure );
		match ( &self.text_input_manager, &self.text_input )
		{
			( Some( manager ), None ) =>
			{
				let seats: Vec<_> = self.seat_state.seats().collect();
				if let Some( seat ) = seats.into_iter().next()
				{
					let ti = manager.get_text_input( &seat, qh, () );
					ti.enable();
					ti.set_content_type( hint, purpose );
					ti.commit();
					self.text_input = Some( ti );
				} else {
					eprintln!( "ltk: activate_text_input: no seat available" );
				}
			}
			( None, _ ) =>
				eprintln!( "ltk: activate_text_input: no text_input_manager (compositor did not advertise zwp_text_input_manager_v3)" ),
			// Focus moved between text fields: refresh the content type (e.g.
			// flag a password field) without re-creating the object.
			( Some( _ ), Some( ti ) ) =>
			{
				ti.set_content_type( hint, purpose );
				ti.commit();
			}
		}
	}

	pub( crate ) fn deactivate_text_input( &mut self )
	{
		if let Some( ti ) = self.text_input.take()
		{
			ti.disable();
			ti.commit();
			ti.destroy();
		}
	}

	pub( crate ) fn reenable_text_input( &self )
	{
		if let Some( ti ) = &self.text_input
		{
			let ( hint, purpose ) = content_type( self.text_input_secure );
			ti.enable();
			ti.set_content_type( hint, purpose );
			ti.commit();
		}
	}

	/// Snapshot a focused-widget's geometry needed by the pointer
	/// hit-testers for text editing. Returns `None` when the widget
	/// isn't a TextEdit or its rect is missing — the helper above
	/// short-circuits in that case.
	pub( crate ) fn text_input_geometry(
		&self,
		focus: SurfaceFocus,
		idx:   usize,
	) -> Option<( Rect, String, bool, bool, crate::widget::text::TextAlign, f32 )>
	{
		let ss = self.surface( focus );
		let widget = find_widget( &ss.widget_rects, idx )?;
		let ( value_handler, multiline, secure, align, font_size ) = match &widget.handlers
		{
			WidgetHandlers::TextEdit { value, multiline, secure, align, font_size, .. } =>
				( value.clone(), *multiline, *secure, *align, *font_size ),
			_ => return None,
		};
		// Prefer the *pending* value (typed-but-not-yet-applied) when
		// it exists — that's what the user sees right now and what
		// the cursor measurements should be relative to.
		let value = ss.pending_text_values.get( &idx )
			.cloned()
			.unwrap_or( value_handler );
		Some( ( widget.rect, value, multiline, secure, align, font_size ) )
	}
}

/// Map a field's `secure` flag to the text-input-v3 content type. Secure
/// fields are flagged `Password` with `SensitiveData | HiddenText` so the
/// IME/OSK skips prediction, autocorrect and storing the value.
fn content_type( secure: bool ) -> ( zwp_text_input_v3::ContentHint, zwp_text_input_v3::ContentPurpose )
{
	if secure
	{
		(
			zwp_text_input_v3::ContentHint::SensitiveData | zwp_text_input_v3::ContentHint::HiddenText,
			zwp_text_input_v3::ContentPurpose::Password,
		)
	} else {
		( zwp_text_input_v3::ContentHint::None, zwp_text_input_v3::ContentPurpose::Normal )
	}
}