Implement soft keyboard visibility on Android
- Added `SoftKeyboardContext` enum and support for calculating keyboard insets from both virtual and platform sources - Updated `AppContext` to provide `soft_keyboard_rect` for determining visible keyboard area - Adjusted UI rendering to shift content when input boxes intersect with the soft keyboard, preventing overlap - Modified `MainActivity` and Android manifest to use `windowSoftInputMode="adjustResize"` and updated window inset handling - Introduced helper functions (`include_input`, `input_rect`, `clear_input_rect`) in `notedeck_ui` for tracking focused input boxes - Fixed Android JNI keyboard height reporting to clamp negative values Together, these changes allow the app to correctly detect and respond to soft keyboard visibility on Android, ensuring input fields remain accessible when typing. Fixes: https://github.com/damus-io/notedeck/issues/946 Fixes: https://github.com/damus-io/notedeck/issues/1043
This commit is contained in:
@@ -59,7 +59,7 @@ impl<'a> AccountLoginView<'a> {
|
||||
let text_edit_width = available_width - button_width;
|
||||
|
||||
let textedit_resp = ui.add_sized([text_edit_width, 40.0], login_textedit(self.manager, self.i18n));
|
||||
input_context(&textedit_resp, self.clipboard, self.manager.input_buffer(), PasteBehavior::Clear);
|
||||
input_context(ui, &textedit_resp, self.clipboard, self.manager.input_buffer(), PasteBehavior::Clear);
|
||||
|
||||
if eye_button(ui, self.manager.password_visible()).clicked() {
|
||||
self.manager.toggle_password_visibility();
|
||||
|
||||
@@ -209,6 +209,7 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
let out = textedit.show(ui);
|
||||
|
||||
input_context(
|
||||
ui,
|
||||
&out.response,
|
||||
self.note_context.clipboard,
|
||||
&mut self.draft.buffer.text_buffer,
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use core::f32;
|
||||
|
||||
use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit};
|
||||
use egui_winit::clipboard::Clipboard;
|
||||
use enostr::ProfileState;
|
||||
use notedeck::{profile::unwrap_profile_url, tr, Images, Localization, NotedeckTextStyle};
|
||||
use notedeck_ui::context_menu::{input_context, PasteBehavior};
|
||||
use notedeck_ui::{profile::banner, ProfilePic};
|
||||
|
||||
pub struct EditProfileView<'a> {
|
||||
state: &'a mut ProfileState,
|
||||
clipboard: &'a mut Clipboard,
|
||||
img_cache: &'a mut Images,
|
||||
i18n: &'a mut Localization,
|
||||
}
|
||||
@@ -16,11 +19,13 @@ impl<'a> EditProfileView<'a> {
|
||||
i18n: &'a mut Localization,
|
||||
state: &'a mut ProfileState,
|
||||
img_cache: &'a mut Images,
|
||||
clipboard: &'a mut Clipboard,
|
||||
) -> Self {
|
||||
Self {
|
||||
i18n,
|
||||
state,
|
||||
img_cache,
|
||||
clipboard,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,14 +100,14 @@ impl<'a> EditProfileView<'a> {
|
||||
)
|
||||
.as_str(),
|
||||
));
|
||||
ui.add(singleline_textedit(self.state.str_mut("display_name")));
|
||||
singleline_textedit(ui, self.state.str_mut("display_name"), self.clipboard);
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label(
|
||||
tr!(self.i18n, "Username", "Profile username field label").as_str(),
|
||||
));
|
||||
ui.add(singleline_textedit(self.state.str_mut("name")));
|
||||
singleline_textedit(ui, self.state.str_mut("name"), self.clipboard);
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
@@ -114,28 +119,28 @@ impl<'a> EditProfileView<'a> {
|
||||
)
|
||||
.as_str(),
|
||||
));
|
||||
ui.add(multiline_textedit(self.state.str_mut("picture")));
|
||||
multiline_textedit(ui, self.state.str_mut("picture"), self.clipboard);
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label(
|
||||
tr!(self.i18n, "Banner", "Profile banner URL field label").as_str(),
|
||||
));
|
||||
ui.add(multiline_textedit(self.state.str_mut("banner")));
|
||||
multiline_textedit(ui, self.state.str_mut("banner"), self.clipboard);
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label(
|
||||
tr!(self.i18n, "About", "Profile about/bio field label").as_str(),
|
||||
));
|
||||
ui.add(multiline_textedit(self.state.str_mut("about")));
|
||||
multiline_textedit(ui, self.state.str_mut("about"), self.clipboard);
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label(
|
||||
tr!(self.i18n, "Website", "Profile website field label").as_str(),
|
||||
));
|
||||
ui.add(singleline_textedit(self.state.str_mut("website")));
|
||||
singleline_textedit(ui, self.state.str_mut("website"), self.clipboard);
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
@@ -147,7 +152,7 @@ impl<'a> EditProfileView<'a> {
|
||||
)
|
||||
.as_str(),
|
||||
));
|
||||
ui.add(multiline_textedit(self.state.str_mut("lud16")));
|
||||
multiline_textedit(ui, self.state.str_mut("lud16"), self.clipboard);
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
@@ -159,7 +164,8 @@ impl<'a> EditProfileView<'a> {
|
||||
)
|
||||
.as_str(),
|
||||
));
|
||||
ui.add(singleline_textedit(self.state.str_mut("nip05")));
|
||||
|
||||
singleline_textedit(ui, self.state.str_mut("nip05"), self.clipboard);
|
||||
|
||||
let Some(nip05) = self.state.nip05() else {
|
||||
return;
|
||||
@@ -208,21 +214,29 @@ fn label(text: &str) -> impl egui::Widget + '_ {
|
||||
}
|
||||
}
|
||||
|
||||
fn singleline_textedit(data: &mut String) -> impl egui::Widget + '_ {
|
||||
TextEdit::singleline(data)
|
||||
.min_size(vec2(0.0, 40.0))
|
||||
.vertical_align(egui::Align::Center)
|
||||
.margin(Margin::symmetric(12, 10))
|
||||
.desired_width(f32::INFINITY)
|
||||
fn singleline_textedit(ui: &mut egui::Ui, data: &mut String, clipboard: &mut Clipboard) {
|
||||
let r = ui.add(
|
||||
TextEdit::singleline(data)
|
||||
.min_size(vec2(0.0, 40.0))
|
||||
.vertical_align(egui::Align::Center)
|
||||
.margin(Margin::symmetric(12, 10))
|
||||
.desired_width(f32::INFINITY),
|
||||
);
|
||||
|
||||
input_context(ui, &r, clipboard, data, PasteBehavior::Clear);
|
||||
}
|
||||
|
||||
fn multiline_textedit(data: &mut String) -> impl egui::Widget + '_ {
|
||||
TextEdit::multiline(data)
|
||||
// .min_size(vec2(0.0, 40.0))
|
||||
.vertical_align(egui::Align::TOP)
|
||||
.margin(Margin::symmetric(12, 10))
|
||||
.desired_width(f32::INFINITY)
|
||||
.desired_rows(1)
|
||||
fn multiline_textedit(ui: &mut egui::Ui, data: &mut String, clipboard: &mut Clipboard) {
|
||||
let r = ui.add(
|
||||
TextEdit::multiline(data)
|
||||
// .min_size(vec2(0.0, 40.0))
|
||||
.vertical_align(egui::Align::TOP)
|
||||
.margin(Margin::symmetric(12, 10))
|
||||
.desired_width(f32::INFINITY)
|
||||
.desired_rows(1),
|
||||
);
|
||||
|
||||
input_context(ui, &r, clipboard, data, PasteBehavior::Clear);
|
||||
}
|
||||
|
||||
fn in_frame(ui: &mut egui::Ui, contents: impl FnOnce(&mut egui::Ui)) {
|
||||
|
||||
@@ -311,7 +311,7 @@ fn search_box(
|
||||
.frame(false),
|
||||
);
|
||||
|
||||
input_context(&response, clipboard, input, PasteBehavior::Append);
|
||||
input_context(ui, &response, clipboard, input, PasteBehavior::Append);
|
||||
|
||||
let mut requested_focus = false;
|
||||
if focus_state == FocusState::ShouldRequestFocus {
|
||||
|
||||
@@ -147,7 +147,7 @@ impl<'a> SettingsView<'a> {
|
||||
"Label for appearance settings section",
|
||||
);
|
||||
settings_group(ui, title, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Font size:",
|
||||
@@ -207,7 +207,7 @@ impl<'a> SettingsView<'a> {
|
||||
|
||||
let current_zoom = ui.ctx().zoom_factor();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Zoom Level:",
|
||||
@@ -260,7 +260,7 @@ impl<'a> SettingsView<'a> {
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Language:",
|
||||
@@ -288,7 +288,7 @@ impl<'a> SettingsView<'a> {
|
||||
});
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Theme:",
|
||||
@@ -441,7 +441,7 @@ impl<'a> SettingsView<'a> {
|
||||
"Label for others settings section"
|
||||
);
|
||||
settings_group(ui, title, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(richtext_small(tr!(
|
||||
self.note_context.i18n,
|
||||
"Sort replies newest first:",
|
||||
|
||||
@@ -237,8 +237,10 @@ fn show_no_wallet(
|
||||
.password(true);
|
||||
|
||||
// add paste context menu
|
||||
let text_edit_resp = ui.add(text_edit);
|
||||
input_context(
|
||||
&ui.add(text_edit),
|
||||
ui,
|
||||
&text_edit_resp,
|
||||
clipboard,
|
||||
&mut state.buf,
|
||||
PasteBehavior::Clear,
|
||||
@@ -388,13 +390,17 @@ fn show_default_zap(
|
||||
};
|
||||
|
||||
let id = ui.id().with("default_zap_amount");
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(text)
|
||||
.desired_width(desired_width)
|
||||
.margin(egui::Margin::same(8))
|
||||
.font(font)
|
||||
.id(id),
|
||||
);
|
||||
|
||||
{
|
||||
let r = ui.add(
|
||||
egui::TextEdit::singleline(text)
|
||||
.desired_width(desired_width)
|
||||
.margin(egui::Margin::same(8))
|
||||
.font(font)
|
||||
.id(id));
|
||||
|
||||
notedeck_ui::include_input(ui, &r);
|
||||
}
|
||||
|
||||
ui.memory_mut(|m| m.request_focus(id));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user